Skip to main content

pkl

Attribute resources to plugins, and release them whenever you decide a lifetime ends.

📚 Documentation · The idea · Quick start · Extensions · Examples

The idea

A plugin system has to answer two questions:

  1. Who is running right now? So that whatever gets created can be attributed to someone.
  2. Who owns what? So that it can all be cleaned up when the plugin goes away.

That is the whole core of pkl, in four small concepts:

Concept What it is
Plugin An identity. Two plugins are the same iff they are the same object. Nothing else: derive, compose or wrap it to attach your own data.
PluginTracker Knows which plugin is currently executing (per thread / asyncio task).
Resource A thing that can be released: anything with a release() method.
ResourceRegistry A map from plugin to resources. release(plugin) releases them all, newest first.

pkl never decides what "enable", "disable" or "uninstall" mean. A registry is a lifetime: you create as many as you like and call release(plugin) whenever you want. Call it on disable and it is a disable lifetime; call it on uninstall and it is an uninstall lifetime. There is no global state: a process can have any number of trackers and registries.

Everything else is an extension: it depends on the core, and the core never imports it.

Installation

Python 3.11+, no dependencies.

uv add plugins-kernel      # or: pip install plugins-kernel

Quick start

from dataclasses import dataclass
from pkl import Plugin, PluginTracker, ResourceRegistry


@dataclass(eq=False)                 # eq=False keeps "same iff `is`"
class AppPlugin(Plugin):             # pkl only sees the identity; the rest is yours
    name: str


plugins = PluginTracker[AppPlugin]()
disable = ResourceRegistry[AppPlugin]()   # a lifetime


class Connection:                    # any object with release() is a resource
    def release(self) -> None:
        print("closed")


alpha = AppPlugin("alpha")
with plugins.executing(alpha):       # alpha is running...
    owner = plugins.require_current()
    disable.register(owner, Connection())

disable.release(alpha)               # ...and now its lifetime ends: prints "closed"

Registering by hand gets tedious, which is what pkl.tracking is for.

Extensions

Extensions are plain modules in the package. They depend on the core (and on tracking), never on each other.

Module Provides
pkl.tracking ResourceTracker, Tracked (resources that register themselves under the plugin that created them) and Callback (a function bound to its creating plugin, as a resource).
pkl.events event_decorator builds the @event decorator: events are invocable only by their owner (protected=False opts out), anyone subscribes, subscriptions die with the subscriber. Generator events run code before/after handlers. emit() awaits async handlers.
pkl.syscall syscall: a function that runs as the plugin that defined it, whoever calls it. Sync and async.
pkl.timing Timer.timeout(...) / Timer.interval(...): cancelled on release, callbacks run as their owner.
pkl.files File, Directory, TempFile, TempDirectory: deleted on release.
pkl.modules ModuleResource: load a file or package under a dotted name, unload it on release.

Bring your own: subclass Tracked, implement on_release, and your resource gets the same automatic tracking.

Automatic tracking

Bind a base class to a lifetime once; every subclass is then recorded under the plugin that creates it:

from pkl.tracking import ResourceTracker, Tracked
from pkl.timing import Timer

runtime = ResourceRegistry[AppPlugin]()


class RuntimeResource(Tracked[AppPlugin], tracker=ResourceTracker(plugins, runtime)): ...


class AppTimer(Timer, RuntimeResource): ...


with plugins.executing(alpha):
    AppTimer.interval(lambda: print("tick"), 1.0)   # attributed to alpha, recorded in `runtime`

runtime.release(alpha)                               # the timer is cancelled

Creating a tracked resource while no plugin is executing raises NoCurrentPluginError, because it could never be released. Pass ResourceTracker(..., allow_orphans=True) for resources that legitimately belong to the host.

One class can also serve several trackers (e.g. one per game zone) without subclassing: Timer.with_tracker(zone.tracker).interval(...). An SDK that must stay single-host forbids that with allow_tracker_override=False.

An SDK for your plugins

If plugin authors only ever import your SDK, tracking is 100% automatic and they never see pkl. See examples/sdk.py: it defines a derived AppPlugin (id, name, metadata), a runtime and a persistent lifetime, the resource classes plugins use, and disable() / uninstall().

Async and threads

The current plugin lives in a ContextVar, so concurrent asyncio tasks that run as different plugins never see each other's plugin, and tasks inherit the plugin they were created under. New threads start with an empty context: bind their entry point with plugins.bind(fn) (it captures the plugin executing now), or self.bind(fn) inside a resource. Timer does this for you.

Examples

python examples/main.py    # two plugins, one host, disable vs uninstall lifetimes
python examples/zones.py   # several independent hosts in one process

Development

uv sync
uv run pytest
uv run mypy --strict pkl tests

License

MIT

Metadata

Release files for plugins-kernel 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for plugins-kernel 0.2.0
File Size Uploaded
plugins_kernel-0.2.0.tar.gz 34.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for plugins-kernel 0.2.0
File Interpreter ABI Platform
plugins_kernel-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 55.8 kB

Release files / plugins_kernel-0.2.0.tar.gz

Download URL plugins_kernel-0.2.0.tar.gz
Size 34.4 kB
Tags Source
SHA-256 checksum
How to use checksums
77db6fa5149df3bf0d869f38fc7af7e343269eeb57b4a3fe7ad89b48f22310b4
BLAKE2b-256 checksum
How to use checksums
b19819b0e126fa1e68bb8a0efb2374522161bb4b08f6718ff948973e6f3547a8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / plugins_kernel-0.2.0-py3-none-any.whl

Download URL plugins_kernel-0.2.0-py3-none-any.whl
Size 21.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
8a90a703120578281cf8bda975510720247ec72706b529b5951f0d70843d5486
BLAKE2b-256 checksum
How to use checksums
d4174adf52927e7fc7a6ed49892d251bbcc48e486a1a504817db6e105d0bacc1
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release history Release notifications | RSS feed

0.2.1

2 release files

This release

0.2.0 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page