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 QgisSdkPluginfrom 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().
Exporting a layer safely
Section titled “Exporting a layer safely”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).