Skip to main content

Contribution points in Python

The same model as the TypeScript runtime: a plugin declares what it offers, the host enumerates and chooses.

from reactor import PluginManifest, PluginPlatform, define_contribution_point

VIEW_TYPE = define_contribution_point("app.viewType")


class NotebookPlugin:
def provide_contributions(self, contributions) -> None:
contributions.contribute(
VIEW_TYPE,
{"title": "Notebook", "route": "/notebook"},
contribution_id="notebook",
order=10,
)


platform = PluginPlatform()
platform.register_plugin(PluginManifest(name="notebook", version="1.0.0"), NotebookPlugin())

for contribution in platform.get_contributions(VIEW_TYPE):
print(contribution.id, contribution.value["title"])

A plugin receives a view bound to its own name, so it contributes as itself rather than passing a name that could be wrong — or borrowed.

Two differences from the TypeScript side

Both deliberate.

Tenants

get_contributions(point, tenant_id=...) filters by what that tenant may use, so enablement is applied where it already lives instead of at every call site. See Tenants and marketplace.

Disable keeps, unregister disposes

platform.disable_plugin("notebook")
platform.get_contributions(VIEW_TYPE) # [] — hidden, not lost
platform.enable_plugin("notebook")
platform.get_contributions(VIEW_TYPE) # back, same objects

platform.unregister_plugin("notebook") # gone for good

The asymmetry is on purpose: the Python platform has no re-register path, so disposing on disable would make a toggle destructive. Contributions are filtered on read instead — which is also where tenant scoping already happens.

Contributing after registration

Through the same bound view:

dispose = platform.contributions_for("notebook").contribute(
VIEW_TYPE, {"title": "Scratch"}, contribution_id="scratch"
)
dispose() # idempotent