wolfhece._plugin_loader ======================= .. py:module:: wolfhece._plugin_loader .. autoapi-nested-parse:: Companion plugin loader. Discovers, validates, and loads companion plugins from a plugins directory. Plugin layout ------------- Each plugin lives in its own subdirectory containing at least two files: ``plugin.toml`` Machine-readable metadata (required). See the schema below. ``companion.py`` Python module that defines (and exports) the companion class (required). Example directory tree:: wolfhece/data/plugins/ ├── _template/ ← ignored (starts with ``_``) │ ├── plugin.toml │ └── companion.py └── my_feature/ ├── plugin.toml └── companion.py ``plugin.toml`` schema ---------------------- .. code-block:: toml [plugin] name = "my_feature" # unique slug (required) display_name = "My Feature" # human-readable label (defaults to name) version = "1.0.0" # semver string (required) author = "Jane Doe" # (required) email = "jane@example.com" # optional description = "Short description." # optional entry_class = "MyFeatureCompanion" # companion class in companion.py (required) created = "2026-01-15" # optional ISO date updated = "2026-05-22" # optional ISO date [compatibility] requires_wolfhece = ">=2.2" # optional, informational only min_python = "3.11" # optional "major.minor" constraint .. warning:: Loading a plugin executes arbitrary Python code from ``companion.py``. Only load plugins from **trusted sources**. Module Contents --------------- .. py:data:: _logger .. py:data:: PLUGINS_DIR :type: pathlib.Path .. py:data:: BUILTIN_PLUGINS_DIR :type: pathlib.Path .. py:data:: _MANIFEST_FILENAME :value: 'plugin.toml' .. py:data:: _COMPANION_FILENAME :value: 'companion.py' .. py:data:: _MODULE_PREFIX :value: '_wolfhece_plugin_' .. py:class:: PluginManifest Parsed metadata from ``plugin.toml``. .. py:attribute:: name :type: str .. py:attribute:: display_name :type: str .. py:attribute:: version :type: str .. py:attribute:: author :type: str .. py:attribute:: entry_class :type: str .. py:attribute:: email :type: str :value: '' .. py:attribute:: description :type: str :value: '' .. py:attribute:: created :type: str :value: '' .. py:attribute:: updated :type: str :value: '' .. py:attribute:: requires_wolfhece :type: str :value: '' .. py:attribute:: min_python :type: str :value: '' .. py:method:: from_dict(data: dict) -> PluginManifest :classmethod: Build from a parsed ``plugin.toml`` dict. .. py:class:: PluginInfo A discovered plugin — manifest, resolved class, and runtime status. .. py:attribute:: manifest :type: PluginManifest .. py:attribute:: path :type: pathlib.Path .. py:attribute:: companion_class :type: type | None :value: None .. py:attribute:: enabled :type: bool :value: True .. py:attribute:: load_error :type: str :value: '' .. py:property:: loaded :type: bool ``True`` if the companion class was successfully imported. .. py:property:: name :type: str .. py:property:: display_name :type: str .. py:exception:: PluginValidationError(path: pathlib.Path, errors: list[str]) Bases: :py:obj:`ValueError` .. autoapi-inheritance-diagram:: wolfhece._plugin_loader.PluginValidationError :parts: 1 :private-bases: Raised when a plugin directory does not pass validation. .. py:attribute:: path .. py:attribute:: errors :type: list[str] .. py:function:: validate_plugin_dir(plugin_dir: pathlib.Path) -> list[str] Return a list of validation error strings (empty list = OK). Checks: * The path is an existing directory. * ``plugin.toml`` exists and is valid TOML. * Required keys ``name``, ``version``, ``author``, ``entry_class`` are present. * ``name`` is a slug-like identifier (only letters, digits, ``-``, ``_``). * ``min_python`` constraint is satisfied by the running interpreter (if set). * ``companion.py`` exists. This function never raises — all problems are returned as strings. .. py:function:: load_plugin(plugin_dir: pathlib.Path) -> PluginInfo Load and validate one plugin from *plugin_dir*. :param plugin_dir: Path to the plugin subdirectory. :raises PluginValidationError: if the directory fails validation. :raises ImportError: if ``companion.py`` cannot be imported. :raises AttributeError: if the entry class is not found in the module. :raises TypeError: if the entry class does not subclass :class:`~wolfhece._menu_companion_abc.AbstractCompanion`. :return: A fully populated :class:`PluginInfo` with ``companion_class`` set. .. py:function:: discover_plugins(directory: pathlib.Path | str | None = None) -> list[PluginInfo] Discover all plugin directories under *directory*. Skips subdirectories whose name starts with ``_`` or ``.`` (template / draft folders) so they can coexist safely with live plugins. Invalid or failing plugins are **not** raised — they are returned as :class:`PluginInfo` objects with :attr:`~PluginInfo.load_error` set and :attr:`~PluginInfo.companion_class` = ``None``. When two valid plugins share the same *name*, the first one (alphabetical) wins and the duplicate is logged then skipped. :param directory: Root plugins directory. Defaults to ``wolfhece/data/plugins``. :return: List of :class:`PluginInfo` objects, one per discovered subdirectory (failures included). .. py:function:: _make_error_info(sub: pathlib.Path, error_msg: str) -> PluginInfo Build a placeholder PluginInfo for a plugin that failed to load. .. py:function:: discover_all_plugins(user_directory: pathlib.Path | str | None = None) -> list[PluginInfo] Discover both built-in and user plugins, returning a combined list. Built-in plugins (from :data:`BUILTIN_PLUGINS_DIR`) are listed first, followed by user plugins (from *user_directory* or :data:`PLUGINS_DIR`). Duplicate plugin names (same slug in both directories) are resolved in favour of the built-in version — the user plugin is skipped with a warning. :param user_directory: Directory to scan for user plugins. Defaults to :data:`PLUGINS_DIR`. :return: Combined list of :class:`PluginInfo` objects.