Skip to main content

Plugin framework with hooks (pyHooky) and tasks (pyWorkflowy).

Project description

pyPlugy

Plugin framework for Python that composes pyHooky for hook-based extension points and (optionally) pyWorkflowy for plugin-owned background tasks.

A plugin in pyPlugy is just a tagged collection of hook registrations (plus, optionally, tasks). The manager drives lifecycle, isolates each plugin's registrations via pyHooky's tag_scope, and tears them all down with one clear_tag call on unload.

Install

uv add pyplugy
uv add 'pyplugy[tasks]'   # add pyWorkflowy integration

pyplugy depends on pyhooky and packaging. The [tasks] extra pulls in pyworkflowy — without it, ctx.task raises a clear error pointing at the extra.

Defining a plugin

Two equivalent authoring forms:

Decorator form

from pyplugy import plugin, PluginContext

@plugin("audit", version="1.0.0", requires=["auth>=1.0"], description="Audit log")
def setup(ctx: PluginContext) -> None:
    @ctx.before("checkout")
    def log_start(cart): ...

    @ctx.after("checkout")
    def log_done(result, cart): ...

    @ctx.on("checkout:step")
    def log_step(step, **kw): ...

Class form

from pyplugy import Plugin, PluginContext

class AuditPlugin(Plugin):
    name = "audit"
    version = "1.0.0"
    requires = ("auth>=1.0",)
    description = "Audit log"

    def on_load(self, ctx: PluginContext) -> None:
        @ctx.before("checkout")
        def log_start(cart): ...

    def on_unload(self, ctx: PluginContext) -> None: ...
    def on_enable(self, ctx: PluginContext) -> None: ...
    def on_disable(self, ctx: PluginContext) -> None: ...

Both produce a Plugin instance with the same PluginManifest.

PluginContext

The PluginContext handed to your setup / lifecycle methods exposes:

Attribute Use
ctx.hooks The pyhooky.HookRegistry hooks land in
ctx.before/after/around/on/on_error/hook Thin auto-tagged pyHooky passthroughs
ctx.task Register tasks via the manager's pyWorkflowy backend
ctx.scheduler Shared scheduler for periodic tasks (Scheduler protocol)
ctx.config Per-plugin config dict (manager-injected)
ctx.logger logging.Logger named pyplugy.<plugin-name>

Every hook registered inside on_load is auto-tagged with the plugin's name because the manager wraps the call in pyhooky.tag_scope(plugin.name).

Discovery

from pathlib import Path
from pyplugy import PluginManager

manager = PluginManager()

# Entry points
manager.load_entry_points("myapp.plugins")

# Directory
manager.load_directory(Path("./plugins"), recursive=True)

# JIT — any of these:
manager.load(MyPlugin)
manager.load(MyPlugin())
manager.load(some_module)
manager.load("my_app.plugins.audit")        # dotted path
manager.load(Path("./extra/audit.py"))      # .py file

Lifecycle

States: UNLOADED → LOADED → ENABLED ↔ DISABLED → UNLOADED.

manager.load(MyPlugin)            # → LOADED, then ENABLED
manager.disable("my-plugin")      # ENABLED → DISABLED (tears down all hooks/tasks)
manager.enable("my-plugin")       # DISABLED → ENABLED (re-runs on_load setup)
manager.unload("my-plugin")       # → UNLOADED (clear_tag, on_unload)
manager.reload("my-plugin")       # unload + load

Disable strategy — pyPlugy tears down a disabled plugin's hooks and tasks (via clear_tag) and re-runs its setup on enable. This keeps the hook registry as the single source of truth and avoids stale half-registered state when a plugin mutates external resources. Plugin authors should treat on_load / setup as idempotent (or at least re-runnable).

Dependencies between plugins

requires accepts a list of "<name><specifier>" strings (PEP 440):

class Reporting(Plugin):
    name = "reporting"
    version = "0.2.0"
    requires = ("audit>=1.0", "auth<3.0")

On load, the manager performs a topological sort:

  • Missing dependency → PluginDependencyError
  • Specifier mismatch → PluginDependencyError
  • Cycle → PluginDependencyError

Tag-based isolation

Every plugin's hooks are tagged with its name via pyhooky.tag_scope. Unload is a single call:

manager.unload("audit")
# under the hood:
#   registry.clear_tag("audit")

You can list a plugin's targets / tasks with:

manager.plugin_targets("audit")   # ["checkout", "checkout:step", ...]
manager.plugin_tasks("audit")     # ["periodic_flush", ...]

Manager-level hooks

The manager fires pyHooky listener hooks for every lifecycle transition:

Target Fires
pyplugy:plugin:load After a plugin reaches LOADED
pyplugy:plugin:enable After enable
pyplugy:plugin:disable After disable
pyplugy:plugin:unload Before the plugin's hooks/tasks are torn down
pyplugy:plugin:error When any lifecycle method raises (plugin, exc)

Listen with pyhooky.on:

from pyhooky import on
from pyplugy import HOOK_PLUGIN_LOAD

@on(HOOK_PLUGIN_LOAD)
def announce(plugin):
    print("loaded:", plugin.manifest.name)

Integration with pyWorkflowy (optional)

ctx.task and ctx.scheduler come from the manager-injected pyWorkflowy surface:

import pyworkflowy
from pyworkflowy.schedule import Scheduler
from pyplugy import PluginManager

manager = PluginManager(tasky=pyworkflowy, scheduler=Scheduler())

@plugin("cron-cleanup", version="0.1.0")
def setup(ctx):
    @ctx.task
    def cleanup(): ...
    ctx.scheduler.every("5m").do(cleanup)

Without a tasky argument, ctx.task raises a clear RuntimeError. pyPlugy itself only depends on a small typing.Protocol defined in pyplugy._tasky_protocol — so tests and apps don't need pyWorkflowy installed.

Errors

Error When
PluginError Base
PluginNotFoundError get / enable / disable / unload on unknown
PluginAlreadyLoadedError Loading a plugin whose name is already loaded
PluginDependencyError Missing dep, version mismatch, cycle
PluginLoadError on_load / on_enable / decorator setup raised
PluginUnloadError on_unload raised
PluginManifestError Invalid manifest (missing name, ...)

Development

uv sync
uv run pytest
uv run ruff check .
uv run pyrefly check

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pyplugy-0.1.0.tar.gz (18.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pyplugy-0.1.0-py3-none-any.whl (23.1 kB view details)

Uploaded Python 3

File details

Details for the file pyplugy-0.1.0.tar.gz.

File metadata

  • Download URL: pyplugy-0.1.0.tar.gz
  • Upload date:
  • Size: 18.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for pyplugy-0.1.0.tar.gz
Algorithm Hash digest
SHA256 8afeee50f6e6d51ce7bb9f125a39c6ee4d14e8607de57cf849d66c7c430cb27d
MD5 e51740959ef5af58c727d418b2440f63
BLAKE2b-256 1476b3365404039313df3f8b2927e58442429d1768351c31f4fb99b2cc84f4dc

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyplugy-0.1.0.tar.gz:

Publisher: release.yml on KilianSen/pyPlugy

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pyplugy-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: pyplugy-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 23.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for pyplugy-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 8c5bc004929cf6f1dfcab36cc4db970709c961d8a21b5b449311942a9548e984
MD5 60af68fd1fcf580ea992837d7fa15458
BLAKE2b-256 e0a1f2ceadcb59e85865ec4d9e09326a970223e0225a1074abff5bdbc065061b

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyplugy-0.1.0-py3-none-any.whl:

Publisher: release.yml on KilianSen/pyPlugy

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page