Plugins¶
Every extension point in Zeython is a ServiceProvider, registered by one
explicit line in main.py — nothing wires itself in behind your back.
Plugins follow the same rule at the package level: installing a plugin
package changes nothing until you register PluginServiceProvider once.
From then on, which packages actually contribute something is driven by
what's pip installed — one line turns on discovery, not one line per
package, the same way Laravel's package auto-discovery or Django's
INSTALLED_APPS works.
Using a plugin¶
# main.py
from zeython import Application, PluginServiceProvider
app = Application()
app.register(PluginServiceProvider)
That's it — every plugin package currently installed gets its provider(s)
registered (and booted, in the normal boot pass) automatically. Uninstall
the package and it stops contributing anything, with no leftover line in
main.py to clean up.
Writing a plugin¶
A plugin is just a pip-installable package that ships a normal
ServiceProvider and declares it under the zeython.plugins entry-point
group in its own pyproject.toml:
# my_zeython_plugin/providers.py
from zeython import ServiceProvider
class MyPluginServiceProvider(ServiceProvider):
def register(self) -> None:
"""Bind services into the container."""
def boot(self) -> None:
"""Run once every provider (including the host app's own) has registered."""
# pyproject.toml, in the plugin package
[project.entry-points."zeython.plugins"]
my_plugin = "my_zeython_plugin.providers:MyPluginServiceProvider"
The entry point's value is an import path to the provider class itself —
exactly what app.register(...) already accepts directly, so there's
nothing plugin-specific about the provider itself.
A plugin that adds routes should do it in its own register(), the same
way the built-in RouteServiceProvider does — zeython routes/about
and the MCP server's list_routes/app_info tools inspect the app right
after every provider's register() phase, not after boot(), which only
runs lazily on the app's first real request (or an explicit app.boot()).
A route added in boot() still works for real traffic, just not for
those introspection tools.
A plugin that needs something another provider bound — the host app's
Database, a Gate — should defer that specific part to its own
boot() instead, for the same reason every built-in provider does:
register() runs for every provider (the host app's and every plugin's)
before any provider's boot() runs, so a plugin can rely on another
provider's binding existing by boot() regardless of registration order.
See Architecture.
A plugin package can declare more than one entry point in the group (e.g.
one provider that adds routes, another that adds a CLI command) — every
entry point under zeython.plugins across every installed package is
discovered and registered.
Failure is loud, not silent¶
A plugin whose provider fails to import, or whose register()/boot()
raises, is not caught and skipped — it fails the app's boot the same way a
broken app.register(...) line in main.py would. A plugin silently
vanishing because of a typo in its own entry-point path, or a bug in its
register(), would be a much worse failure mode than a loud stack trace
at startup.
Inspecting what's discovered¶
discover_plugins() is the plain function PluginServiceProvider calls
internally — useful on its own for a zeython about-style diagnostic, or
in a test asserting a particular plugin is (or isn't) installed. See
CLI Reference.