registry.py
python
| 1 | """Plugin registry — maps domain names to :class:`~muse.domain.MuseDomainPlugin` instances. |
| 2 | |
| 3 | Every CLI command that operates on domain state calls :func:`resolve_plugin` |
| 4 | once to obtain the active plugin for the current repository. Adding support |
| 5 | for a new domain requires only two changes: |
| 6 | |
| 7 | 1. Implement :class:`~muse.domain.MuseDomainPlugin` in a new module under |
| 8 | ``muse/plugins/<domain>/plugin.py``. |
| 9 | 2. Register the plugin instance in ``_REGISTRY`` below. |
| 10 | |
| 11 | The domain for a repository is stored in ``.muse/repo.json`` under the key |
| 12 | ``"domain"``. Repositories created before this key was introduced default to |
| 13 | ``'midi'``. |
| 14 | """ |
| 15 | |
| 16 | import json |
| 17 | import pathlib |
| 18 | |
| 19 | from muse.core.errors import MuseCLIError |
| 20 | from muse.core.schema import DomainSchema |
| 21 | from muse.domain import MuseDomainPlugin |
| 22 | from muse.plugins.code.plugin import CodePlugin |
| 23 | from muse.plugins.midi.plugin import MidiPlugin |
| 24 | from muse.plugins.scaffold.plugin import ScaffoldPlugin |
| 25 | |
| 26 | _REGISTRY: dict[str, MuseDomainPlugin] = { |
| 27 | "code": CodePlugin(), |
| 28 | "midi": MidiPlugin(), |
| 29 | "scaffold": ScaffoldPlugin(), |
| 30 | } |
| 31 | |
| 32 | _DEFAULT_DOMAIN = "midi" |
| 33 | |
| 34 | |
| 35 | def _read_domain(root: pathlib.Path) -> str: |
| 36 | """Return the domain name stored in ``.muse/repo.json``. |
| 37 | |
| 38 | Falls back to ``'midi'`` for repos that pre-date the ``domain`` field. |
| 39 | """ |
| 40 | repo_json = root / ".muse" / "repo.json" |
| 41 | try: |
| 42 | data = json.loads(repo_json.read_text()) |
| 43 | domain = data.get("domain") |
| 44 | return str(domain) if domain else _DEFAULT_DOMAIN |
| 45 | except (OSError, json.JSONDecodeError): |
| 46 | return _DEFAULT_DOMAIN |
| 47 | |
| 48 | |
| 49 | def resolve_plugin(root: pathlib.Path) -> MuseDomainPlugin: |
| 50 | """Return the active domain plugin for the repository at *root*. |
| 51 | |
| 52 | Reads the ``"domain"`` key from ``.muse/repo.json`` and looks it up in |
| 53 | the plugin registry. Raises :class:`~muse.core.errors.MuseCLIError` if |
| 54 | the domain is not registered. |
| 55 | |
| 56 | Args: |
| 57 | root: Repository root directory (contains ``.muse/``). |
| 58 | |
| 59 | Returns: |
| 60 | The :class:`~muse.domain.MuseDomainPlugin` instance for this repo. |
| 61 | |
| 62 | Raises: |
| 63 | MuseCLIError: When the domain stored in ``repo.json`` is not in the |
| 64 | registry. This is a configuration error — either the plugin was |
| 65 | not installed or ``repo.json`` was edited manually. |
| 66 | """ |
| 67 | domain = _read_domain(root) |
| 68 | plugin = _REGISTRY.get(domain) |
| 69 | if plugin is None: |
| 70 | registered = ", ".join(sorted(_REGISTRY)) |
| 71 | raise MuseCLIError( |
| 72 | f"Unknown domain {domain!r}. Registered domains: {registered}" |
| 73 | ) |
| 74 | return plugin |
| 75 | |
| 76 | |
| 77 | def read_domain(root: pathlib.Path) -> str: |
| 78 | """Return the domain name for the repository at *root*. |
| 79 | |
| 80 | This is the same lookup used internally by :func:`resolve_plugin`. |
| 81 | Use it when you need the domain string to construct a |
| 82 | :class:`~muse.domain.SnapshotManifest` for a stored manifest. |
| 83 | """ |
| 84 | return _read_domain(root) |
| 85 | |
| 86 | |
| 87 | def registered_domains() -> list[str]: |
| 88 | """Return the sorted list of registered domain names.""" |
| 89 | return sorted(_REGISTRY) |
| 90 | |
| 91 | |
| 92 | def schema_for(domain: str) -> DomainSchema | None: |
| 93 | """Return the ``DomainSchema`` for *domain*, or ``None`` if not registered. |
| 94 | |
| 95 | Allows the CLI and merge engine to look up a domain's schema without |
| 96 | holding a plugin instance. Returns ``None`` rather than raising so callers |
| 97 | can decide whether an unknown domain is an error or a soft miss. |
| 98 | |
| 99 | Args: |
| 100 | domain: Domain name string (e.g. ``'midi'``). |
| 101 | |
| 102 | Returns: |
| 103 | The :class:`~muse.core.schema.DomainSchema` declared by the plugin, |
| 104 | or ``None`` if *domain* is not in the registry. |
| 105 | """ |
| 106 | plugin = _REGISTRY.get(domain) |
| 107 | if plugin is None: |
| 108 | return None |
| 109 | return plugin.schema() |