Skip to content

Plugin SDK (qgis-sdk)

Plugin SDK — qgis-sdk

qgis-sdk is a Python framework for building QGIS plugins — now with a Rust-native CLI (qgis-plugin) and optional Rust acceleration, all installable from pip or conda-forge at native speed.

  • pip: pip install qgis-sdk → import qgis_sdk + qgis-plugin binary on PATH
  • conda: conda install -c conda-forge qgis-sdk → same, with QGIS backend

qgis-rs (rendering/tiling) can stay alongside — both packages share the same Rust workspace and both ship Rust binaries.

Installation

Terminal window
pip install qgis-sdk
# or
conda install -c conda-forge qgis-sdk
pixi add qgis-sdk
# Test
python -c "import qgis_sdk; print(qgis_sdk.__version__, qgis_sdk.HAS_RUST)"
qgis-plugin --help
qgis-plugin version

From source:

Terminal window
git clone https://github.com/Archont561/qgis-rs
cd qgis-rs
pip install maturin
(cd py-packages/qgis-sdk && maturin develop)
python -m pytest py-packages/qgis-sdk/tests -v

Python API

from qgis_sdk import Plugin, action, toolbar, menu
class MyPlugin(Plugin):
name = "My Plugin"
version = "0.1.0"
description = "Does useful things"
author = "Your Name"
email = "you@example.com"
qgis_min_version = "3.28"
category = "Vector"
@toolbar("My Toolbar")
@action(tooltip="Run my tool", icon="icons/tool.svg")
def run_tool(self, iface):
layer = iface.activeLayer()
print(layer.name(), layer.featureCount())
@menu("Plugins", "My Plugin", "Settings")
def open_settings(self, iface):
...
# metadata.txt generation (Rust-accelerated when _core present)
print(MyPlugin.metadata_txt())
MyPlugin.write_metadata("./my_plugin")

Processing algorithm:

from qgis_sdk import Algorithm, parameter, output
class BufferAdvanced(Algorithm):
id = "my_plugin:buffer_advanced"
name = "Advanced Buffer"
group = "Vector geometry"
input_layer = parameter.source("Input layer")
distance = parameter.distance("Buffer distance", default=10.0)
dissolve = parameter.boolean("Dissolve results", default=False)
output_layer = output.sink("Buffered")
def process(self, context):
distance = context.get(self.distance)
# ...
return {self.output_layer.name: distance}

CLI — Rust-native

Terminal window
# Scaffold
qgis-plugin new my_plugin --type processing --rust
qgis-plugin new my_plugin --author "Your Name" --email "you@example.com"
# Validate
qgis-plugin validate
qgis-plugin validate ./my_plugin
qgis-plugin info ./my_plugin --json
# Build, test, install
qgis-plugin build
qgis-plugin test
qgis-plugin install
qgis-plugin dev --rust --launch
# Package for QGIS Plugin Repository
qgis-plugin package -o dist/
qgis-plugin publish --zip dist/my_plugin-0.1.0.zip --dry-run
# Rust acceleration
qgis-plugin rust init
qgis-plugin rust build --release
# Via Python module (same speed — uses Rust extension directly)
python -m qgis_sdk.cli new my_plugin
python -m qgis_sdk.cli validate
qgis-sdk version # alias

Commands

CommandPure Rust?Description
new✅Scaffold new plugin
build✅Build package
test✅Run tests
install✅Install into QGIS profile
dev✅Watch mode
package✅Create .zip
publish✅Upload to repo
validate✅Check structure
info✅Show metadata
rust init✅Add Rust module
rust build✅Build Rust module
version✅Print version

Architecture

qgis-sdk wheel
├── qgis_sdk/
│ ├── __init__.py → Python API (Plugin, Algorithm, metadata)
│ ├── _core.so → Rust cdylib (PyO3) — metadata, validation, scaffolding at native speed
│ ├── _fallback_cli.py → pure-Python fallback when _core not built
│ ├── scaffold.py → pure-Python scaffolding fallback
│ ├── cli.py → Python CLI wrapper (uses _core directly)
│ ├── plugin.py → Plugin base class
│ ├── algorithm.py → Algorithm base class
│ └── runtime.py → lazy PyQGIS access
└── bin/
├── qgis-plugin → Rust binary (native speed)
└── qgis-sdk → alias binary
  • Rust crate: crates/qgis-sdk/Cargo.toml — lib _core + bins qgis-plugin, qgis-sdk, reached from py-packages/qgis-sdk/pyproject.toml via [tool.maturin].manifest-path
  • Python: src/qgis_sdk/ with pyproject.toml using maturin
  • Conda: conda-recipe/meta.yaml builds Rust binaries + Python wheel

UI, Typed Bridge, and Testing

UI Dialogs + WebEngine

Terminal window
qgis-plugin new my_plugin --web --framework react
# Generates ui/main_dialog.ui + dialogs/main_dialog.py + web/map.html + web/bridge.d.ts + frontend/

See Plugin Development Guide and Web Frameworks Guide.

Typed Bridge — @qgis-sdk/bridge

Terminal window
npm install @qgis-sdk/bridge
qgis-plugin bridge generate --bridge my_plugin.dialogs.web_dialog:Bridge --output web/bridge.d.ts
import { createBridge } from '@qgis-sdk/bridge';
import type { Bridge } from './web/bridge.d.ts';
const bridge = await createBridge<Bridge>(); // auto-injects qrc:///qtwebchannel/qwebchannel.js

React: import { useQgisBridge } from '@qgis-sdk/bridge/react'
Vue: import { useQgisBridge } from '@qgis-sdk/bridge/vue'
Web Components: <qgis-bridge> + import '@qgis-sdk/bridge/webcomponents'

See Typed Bridge Guide.

Testing without QGIS

qgis_sdk.testing provides fakes + pytest fixtures:

tests/conftest.py
pytest_plugins = ["qgis_sdk.testing"]
def test_toolbar(fake_iface, fake_action_factory):
from my_plugin import MyPlugin
MyPlugin.action_factory = staticmethod(fake_action_factory)
plugin = MyPlugin(fake_iface)
plugin.init_gui()
assert len(fake_iface.toolbar_icons) == 1
def test_bridge(fake_bridge):
assert fake_bridge.get_layer()["name"] == "test_layer"

Fakes: FakeIface, FakeAction, FakeDialog, FakeWebView, FakeBridge, mock_features(), mock_source().

Terminal window
python -m pytest py-packages/qgis-sdk/tests -q # 112 tests, no QGIS

See Testing Fixtures Guide.

Conda-forge

See py-packages/qgis-sdk/conda-recipe/meta.yaml. It builds Rust binaries and Python extension and installs both.

To publish:

  1. Fork https://github.com/conda-forge/staged-recipes
  2. Copy conda-recipe/ to recipes/qgis-sdk/
  3. Open PR