Skip to content

Replace directory-scanning plugin system with entry points #24

Description

@amorfinv

Current situation

The plugin system (minisky/plugin/plugin.py) is inherited from BlueSky and works by scanning a directory:

  • Plugin.find_plugins() resolves a plugin_path setting (default plugins, currently example_plugins in settings.toml), inserts its parent into sys.path, and globs **/*.py.
  • Each file is parsed with ast (without importing) to find an init_plugin() function, walking the AST backwards from the return statement to reconstruct the config dict and stack commands (Plugin._parse_init_plugin).
  • load_enabled() then imports and initializes the plugins listed in enabled_plugins.

Downsides of this approach:

  • sys.path manipulation — the plugin directory's parent is prepended to sys.path, which can shadow installed packages and makes module names dependent on directory layout.
  • Fragile AST metadata extraction_parse_init_plugin only understands literal dicts assigned or returned in specific shapes; any indirection (config built in a helper, non-literal values) silently loses metadata. It's ~100 lines of code that exists only to avoid importing the module.
  • No packaging story — a plugin can't be pip/uv-installed as its own distribution with its own dependencies; it has to live in a directory MiniSky is pointed at. The tangram plugin already wants this (it has its own optional dependencies and workspace).
  • Path resolution guesswork — relative plugin_path is tried against the package root and then the CWD, so behavior depends on where you launch from.

Proposal

Replace directory scanning with the standard entry points mechanism, discovered via importlib.metadata.entry_points() (stdlib, no new dependency).

A plugin becomes a normal installable package that declares itself in its pyproject.toml:

[project.entry-points."minisky.plugins"]
tangram = "minisky_tangram:init_plugin"

Discovery then becomes roughly:

from importlib.metadata import entry_points

for ep in entry_points(group="minisky.plugins"):
    ...
  • discover() lists entry points in the minisky.plugins group — name and distribution metadata are available without importing anything, so the AST parser can be deleted entirely.
  • Plugin.load() calls ep.load()() and registers hooks/stack commands exactly as _load() does today; the init_plugin() contract (config dict + optional stack functions) can stay unchanged.
  • enabled_plugins in settings.toml keeps working, now naming entry points instead of scanned files.
  • In-repo example plugins can register their entry points in MiniSky's own pyproject.toml (or become a small workspace package), so uv sync makes them discoverable with no plugin_path needed.

Benefits

  • Plugins are real packages: own dependencies, versioning, installable from PyPI/git (uv add minisky-tangram), testable in isolation.
  • Deletes find_plugins + _parse_init_plugin and the sys.path insertion — a large net code reduction, in line with the project's minimalism goal.
  • One less setting to explain (plugin_path goes away).

Open questions

  • Do we keep any directory-based fallback for quick single-file hacking, or is "make it a package" acceptable friction? (uv makes a minimal package cheap: uv init --lib.)
  • Should PLUGINS LIST show all installed entry points or only enabled ones?
  • What to do with the docstring/stack-command preview currently extracted by AST — entry point metadata gives us the distribution's summary, which may be enough.

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions