Skip to content

Scoped plugin runtime

PluginRuntime owns one application for its lifetime. Each runtime has an independent context and resource scope, even when two runtimes use the same app:

from qgis_sdk import QgisSdkPlugin
from qgis_sdk.core import PluginContext, PluginRuntime, ResourceScope
app = QgisSdkPlugin(id="example.tools", title="Example tools")
@app.action(id="run", title="Run")
def run(ctx: PluginContext, value: int) -> int:
return value
context = PluginContext(notifications=..., scope=ResourceScope())
runtime = PluginRuntime(app, context)
runtime.start()
runtime.actions["run"](3)
runtime.unload()

The host supplies PluginContext; it is injected into each handler and never exposed as a bridge input. Register scope cleanup through ctx.scope and use explicit fake contexts to test handlers directly. The runtime uses a private lowering implementation; authors do not construct a plugin class.

PluginRuntimeState is new, running, stopped or failed. start() is valid only from new; starting twice or restarting a stopped runtime raises QgisSdkPluginLifecycleError. unload() is valid from new and running, closes the context scope and drops the instance and action bindings. Repeated unload() calls are no-ops, including after a failed startup.

Ownership stays with the existing ResourceScope: actions, signal connections, dialogs/docks and work register cleanup through ctx.scope. A partial startup failure closes that scope, releases resources in reverse registration order, sets the runtime to failed and re-raises the original author error; a cleanup failure is attached as its __cause__. If cleanup fails during unload, the runtime still reaches stopped, all remaining callbacks run, and the failures are raised as an ExceptionGroup. Scope closure is not proof that in-flight work stopped, and the runtime does not create Qt actions, dialogs, docks or jobs.

Quiescing before a replacement (decision-15, decision-21). A development host that wants to swap the revision in place asks first: runtime.quiesce() returns a QuiesceOutcome; safe_to_replace means everything the plugin owns reached a safe point, refused names the blockers. The answer comes from the scope: owners that hold work register a check with scope.on_quiesce(check, resume=...), and scope.quiesce() asks children first (a view’s work before the plugin’s), then every registered owner, and combines all blockers so the host can report them at once. A root JobService registers itself on the scope it is built on, so jobs are cancelled and ticked up to the bound and anything still waiting on a worker handoff is named by its handle id (job sum#1 is waiting on an unresolved worker handoff). A refusal leaves the runtime running; runtime.resume() (scope.resume()) lets every owner accept work again. A runtime that is not running refuses with the plugin is not running (state new). Nothing here kills a thread or closes a scope: the host decides whether to unload().

ctx.layers.export_vector(layer_id, destination, overwrite=False) writes a GeoJSON or GeoPackage export. It refuses the source dataset itself, including relative paths, symbolic links, hard links and the containing GeoPackage, even when overwrite=True. If the source is missing, changed or cannot be identified, the export is refused. The native adapter currently verifies direct local OGR GeoJSON, GeoPackage and ESRI Shapefile sources; indirect sources such as VRT and other unverified providers are refused rather than guessed. Opening those sources is unchanged. Choose a verified local source before exporting. If a source symlink is retargeted or a relative source resolves differently after the working directory changes, reopen the intended dataset (preferably by absolute path) before exporting.

The output is staged beside the destination and the source identity is checked again before commit. With no overwrite permission, publication requires filesystem hard-link support and atomically refuses an output created during the write. With overwrite=True, a different existing file may be replaced. Keep source and destination paths stable during an export: the checks cannot prevent another process changing the filesystem between the final check and commit. These guarantees apply to Layers.export_vector, not arbitrary Processing output destinations (see the separate Processing safety limitations).