diff --git a/lib/pdwidgets/__init__.py b/lib/pdwidgets/__init__.py index 15f17a7..e6ef01e 100644 --- a/lib/pdwidgets/__init__.py +++ b/lib/pdwidgets/__init__.py @@ -141,6 +141,17 @@ def __getattr__(name): + """Lazily import a widget class listed in ``_LAZY``. + + Args: + name: Public widget or alias name (for example ``"Button"``). + + Returns: + The resolved class or object. + + Raises: + AttributeError: If ``name`` is not a known lazy export. + """ spec = _LAZY.get(name) if spec is None: raise AttributeError(f"module 'pdwidgets' has no attribute {name!r}") @@ -152,4 +163,5 @@ def __getattr__(name): def __dir__(): + """Return sorted public names including lazy widget exports.""" return sorted(set(__all__) | set(globals().keys())) diff --git a/lib/pdwidgets/_themes.py b/lib/pdwidgets/_themes.py index 73bd3a4..bf96958 100644 --- a/lib/pdwidgets/_themes.py +++ b/lib/pdwidgets/_themes.py @@ -36,19 +36,20 @@ class IconTheme: _menu = "menu_" def __init__(self, path=None): - """ - Manage the curated icon set shipped as Python modules under - ``pdwidgets.icons``. + """Create an icon theme that resolves shipped ``pdwidgets.icons`` modules. Module stems look like ``home_filled_36dp`` (sizes from - :data:`ICON_SIZE`: 18, 24, 36, 48). ``path`` is accepted for - compatibility and ignored — icons are always loaded by module name. + :data:`~pdwidgets.ICON_SIZE`: 18, 24, 36, 48). Access icons via + attribute callables such as ``icon_theme.home(ICON_SIZE.LARGE)``. - Usage:: + Args: + path: Accepted for call-site compatibility and ignored; icons are + always loaded by module name. - from pdwidgets import IconTheme, ICON_SIZE, IconButton - icon_theme = IconTheme() - IconButton(screen, icon_file=icon_theme.home(ICON_SIZE.LARGE)) + Example: + >>> from pdwidgets import IconTheme, ICON_SIZE, IconButton + >>> icon_theme = IconTheme() + >>> IconButton(screen, icon_file=icon_theme.home(ICON_SIZE.LARGE)) """ del path # retained for call-site compatibility @@ -59,6 +60,19 @@ def _icon(self, name, size): return f"{self._pkg}.{stem}" def __getattr__(self, name): + """Return a ``(size) -> module_path`` callable for a named icon. + + Args: + name: Icon stem without size or ``dp`` suffix (for example + ``"home"``, ``"menu"``, ``"close"``). + + Returns: + A callable that takes an :data:`~pdwidgets.ICON_SIZE` value and + returns the fully qualified icon module path. + + Raises: + AttributeError: If ``name`` is private (starts with ``_``). + """ if name.startswith("_"): raise AttributeError(name) return lambda size: self._icon(name, size) @@ -87,6 +101,12 @@ class ColorTheme: """ def __init__(self, pal): + """Build semantic color slots from a palette's ``color565`` converter. + + Args: + pal: A :class:`~palettes.Palette` (or compatible) object used only + for ``color565`` conversion and byteswap handling. + """ c = pal.color565 # Neutrals self.background = c(0xF2EEE3) # warm cream page diff --git a/lib/pdwidgets/display.py b/lib/pdwidgets/display.py index 8904952..17985aa 100644 --- a/lib/pdwidgets/display.py +++ b/lib/pdwidgets/display.py @@ -390,11 +390,29 @@ def render_dirty_widgets(self): stack.extend(reversed(self._dirty_children_z_order(widget))) def __getattr__(self, name): + """Forward unknown attributes to the underlying ``display_drv``. + + Args: + name: Attribute name to look up on ``display_drv``. + + Returns: + The value from ``display_drv``. + + Raises: + AttributeError: If neither this object nor ``display_drv`` has + ``name``. + """ if name in _display_drv_get_attrs: return getattr(self.display_drv, name) raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'") def __setattr__(self, name, value): + """Forward known driver attributes to ``display_drv``, else set locally. + + Args: + name: Attribute name. + value: Value to assign. + """ if name in _display_drv_set_attrs: return setattr(self.display_drv, name, value) super().__setattr__(name, value) diff --git a/lib/pdwidgets/task.py b/lib/pdwidgets/task.py index 014e1f4..edbe2d5 100644 --- a/lib/pdwidgets/task.py +++ b/lib/pdwidgets/task.py @@ -28,6 +28,12 @@ def my_callback(): """ def __init__(self, callback, delay): + """Schedule the first run ``delay`` ms from now. + + Args: + callback: Zero-argument callable to invoke on each run. + delay: Interval between runs, in milliseconds. + """ self.callback = callback self.delay = delay self.next_run = ticks_add(ticks_ms(), delay) diff --git a/lib/pdwidgets/widget.py b/lib/pdwidgets/widget.py index a5c0ede..60f4381 100644 --- a/lib/pdwidgets/widget.py +++ b/lib/pdwidgets/widget.py @@ -87,9 +87,15 @@ def __init__( self._register_callbacks() def __str__(self): + """Return a short ``ID `` label for debugging.""" return f"ID {self.id} {self.__class__.__name__}" def __format__(self, format_spec): + """Format like :meth:`__str__`, applying ``format_spec`` to the class name. + + Args: + format_spec: Format specifier applied to the class name portion. + """ return f"ID {self.id} {self.__class__.__name__:{format_spec}}" def _register_callbacks(self): diff --git a/lib/pdwidgets/widgets/button.py b/lib/pdwidgets/widgets/button.py index 5d35b3a..99c28e3 100644 --- a/lib/pdwidgets/widgets/button.py +++ b/lib/pdwidgets/widgets/button.py @@ -194,6 +194,12 @@ def backdrop(self): @backdrop.setter def backdrop(self, value): + """Set the clear color under the face, or ``None`` for parent erase. + + Args: + value: Fill color for ``area`` before the face is painted, or + ``None`` to call ``parent.draw(self.area)`` instead. + """ self._backdrop = value self.invalidate() diff --git a/lib/pdwidgets/widgets/password_field.py b/lib/pdwidgets/widgets/password_field.py index cc77018..b145737 100644 --- a/lib/pdwidgets/widgets/password_field.py +++ b/lib/pdwidgets/widgets/password_field.py @@ -31,6 +31,27 @@ def __init__( max_length=None, mask="*", ): + """Initialize a password field that masks displayed glyphs. + + Args: + parent: Parent widget or screen. + x: Relative x-coordinate. + y: Relative y-coordinate. + w: Width in pixels (defaults to parent width). + h: Height in pixels (defaults from ``text_height``). + align: Alignment constant from :data:`~pdwidgets.ALIGN`. + align_to: Widget to align against (default parent). + fg: Foreground / text color. + bg: Background / field fill color. + visible: Initial visibility (default ``True``). + value: Initial password string (default empty). + padding: ``(left, right, top, bottom)`` inset. + hint: Placeholder text when the value is empty. + text_height: Romfont height (``TEXT_SIZE`` member). + radius: Corner radius of the field border. + max_length: Optional maximum character count. + mask: Single character used to mask each glyph (default ``"*"``). + """ self.mask = mask super().__init__( parent, diff --git a/mkdocs.yml b/mkdocs.yml index 00ca575..1517162 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -41,7 +41,7 @@ plugins: - mkdocstrings: handlers: python: - paths: [src] + paths: [lib] options: show_source: false show_root_heading: true diff --git a/scripts/mkdocs_gen_ref_pages.py b/scripts/mkdocs_gen_ref_pages.py index 392f8fb..01c5bc8 100644 --- a/scripts/mkdocs_gen_ref_pages.py +++ b/scripts/mkdocs_gen_ref_pages.py @@ -1,4 +1,7 @@ -"""Generate mkdocstrings API reference stubs and navigation for pdwidgets.""" +"""Generate mkdocstrings API reference stubs and navigation for pdwidgets. + +Walks ``lib/pdwidgets/``. Skips private modules and generated ``icons/`` pages. +""" from pathlib import Path @@ -6,13 +9,16 @@ nav = mkdocs_gen_files.Nav() root = Path(__file__).parent.parent -src = root / "src" +lib = root / "lib" reference = Path("reference") +SKIP_DIR_NAMES = {"icons", "__pycache__"} -for path in sorted((src / "pdwidgets").rglob("*.py")): +for path in sorted((lib / "pdwidgets").rglob("*.py")): + if any(part in SKIP_DIR_NAMES for part in path.parts): + continue if path.name.startswith("_") and path.name != "__init__.py": continue - module_path = path.relative_to(src).with_suffix("") + module_path = path.relative_to(lib).with_suffix("") doc_path = module_path.with_suffix(".md") full_doc_path = reference / doc_path @@ -22,7 +28,7 @@ parts = parts[:-1] doc_path = doc_path.with_name("index.md") full_doc_path = full_doc_path.with_name("index.md") - elif parts[-1] == "__main__": + if not parts or parts[-1] == "__main__" or any(part.startswith("_") for part in parts): continue nav[parts] = doc_path.as_posix()