Project checks
qgis-sdk check [PATH] reads a plugin project and reports what would stop it
from loading in QGIS or from being inspected without QGIS. It is non-mutating
and QGIS-free by design (decision-8): it imports nothing from the project,
installs nothing and runs no handler. The command is a thin layer over
qgis_sdk_tools.check.check_project(root, native_modules=...), which qgis-sdk build
will call in-process rather than through a subprocess.
qgis-sdk check # the working directoryqgis-sdk check my_tools --json # the same report as dataqgis-sdk check my_tools --native "my_tools.native.*"qgis-sdk check my_tools --imports # every import root, with its bucketEvery finding is a Diagnostic with a stable rule, a root-relative path,
1-based line/column (0 for whole-file findings), a message and a
remediation; text output prints path:line:col: RULE message with the
remediation indented beneath, and ends with checked N files: .... The exit
code is 1 when any error was found and 2 when the path is not a directory.
--imports prints the bucket of every import root, and every finding below
still prints with it:
json stdlib field_tools/plugin.py:5qgis host field_tools/plugin.py:6qgis_sdk runtime field_tools/plugin.py:4fixture_pure vendored field_tools/plugin.py:7pandas external field_tools/plugin.py:8mystery_package unknown field_tools/plugin.py:9The buckets are stdlib, host (QGIS, Qt, processing, osgeo),
runtime (qgis_sdk), plugin (the package itself), vendored (a
distribution declared under [tool.qgis-sdk.build] vendor), external (a
root declared under external) and unknown. Only unknown costs anything:
it is DEP001, and it is reported here rather than first at build
(decision-50 point 3). --json carries the same listing under imports
(counts per bucket plus one entry per root).
| Rule | Meaning |
|---|---|
QGIS001 |
qgis, PyQt5/6 or PySide2/6 is imported when the module loads: module scope, a class body, a try, a with or a module-level if all count. Imports inside a function and under if TYPE_CHECKING: are fine. |
ARCH001 |
qgis_sdk.adapters.qgis (the PyQGIS-backed adapters) is imported when a pure module loads; let host() build the context, or reach the adapter inside the function that runs in QGIS. |
PROJECT001 |
No directory with an __init__.py under the root (other than tests/). |
PROJECT002 |
No pyproject.toml, or no [tool.qgis-sdk] plugin = "<package>.plugin:app" in it; build and dev derive metadata.txt and classFactory from that line (decision-43). |
CONFIG001 |
[tool.qgis-sdk], [tool.qgis-sdk.check], [tool.qgis-sdk.web] or [tool.qgis-sdk.metadata] in pyproject.toml is malformed (plugin not pkg.module:app, placement not dock/dialog, grants not a list of strings, a passthrough value that is not a scalar). |
META001 |
The plugin package has a hand-written metadata.txt; build renders it, so delete the file (or move extra keys under [tool.qgis-sdk.metadata]). |
META002 |
pyproject.toml lacks a key metadata.txt needs: [project] name, version, description, authors[0].name/.email, or [tool.qgis-sdk] qgis-minimum-version. |
ENTRY002 |
A module of the plugin package defines or imports classFactory; build and dev bind it through host(). |
GRANT001 |
[tool.qgis-sdk.web] grants names something that is neither a host grant (layers.read, selecting every operation that needs it) nor a host operation id (project.layer_ids); see qgis_sdk.bridge.host.granted_operations. |
VERSION003 (warning) |
pyproject.toml declares no qgis-sdk-runtime (or qgis-sdk) requirement; build ships the installed runtime and says so in the NOTICE. |
VERSION004 |
The installed qgis-sdk-runtime does not satisfy the project’s requirement. The finding names both and the pip install line; the tools never install. |
UI001 |
A method of a Dialog/Dock subclass constructs an interactive Qt or QGIS widget (QPushButton, QLineEdit, QSpinBox, QgsMapLayerComboBox, … - qgis_sdk_tools.check.ui.INTERACTIVE_WIDGETS) and never calls setObjectName on the name holding it; a constructor passed straight to addWidget is flagged too. Name it, or use a qgis_sdk.ui component, which names its widget (form/<field>, buttons/<name>). |
UI002 |
A Dialog/Dock subclass with ui = "file.ui" has an @on(name) whose name is not an objectName the Designer file declares, or the file is not next to the module. The wiring would refuse the surface when it opens; the finding lists the names the file does declare. |
UI003 |
An application action carrying @toolbar/@menu/@shortcut takes inputs beyond ctx; a button collects nothing. Drop the inputs, or bind a surface that collects them and calls self.ctx.actions.<id>(...). |
UI004 (warning) |
A module of the plugin package that is declared native still imports qgis.PyQt, PyQt5/6 or PySide2/6 when it loads: the exemption lifts QGIS001, but Qt at module level defeats the fakes (build_surface, ui_snapshot) and check’s inspection of that module. Keep Qt inside build(); the catalogue needs no Qt import. |
UI005 |
The declarative form, read statically: @on(attribute) in a Dialog/Dock subclass names a class attribute that is a string, a number or a literal container rather than a component, or a bare name the class never binds; or a Custom declared on the class holds a widget built when the class is defined (Custom(widget=QLabel(...))), which cannot be copied for each window. Assign a catalogue component to the attribute (or pass the objectName as a string), and give Custom the widget class or a factory. A call the checker cannot see into and a module-level name are left to the host. |
ICON001 |
@toolbar(icon="...") names something that is not a file under <package>/icons/ (a bare file name is required). |
JOB001 |
A @job body reaches GUI state: iface (imported at module scope or inside the body), a Qt or QGIS GUI class (QgsMapCanvas, QMessageBox, … - qgis_sdk_tools.check.jobs.GUI_CLASSES), QgsProject.instance(), or a module-level name that holds one of those. The whole body runs on a worker thread, so do the GUI work on the driving thread: prepare before submitting, apply in the success callback. |
JOB002 |
A @job body reads a context attribute other than progress and cancellation; it is handed a WorkerContext, which refuses every driving-thread capability by name, so the job would fail on its first step. Pass plain values in and re-read the service from the callback. |
JOB003 (warning) |
A @job body imports a module by name (importlib.import_module, __import__), which check cannot follow: the body may reach GUI state through it. Import it at module level or in a helper outside the job. |
HOST001 |
A Desktop-only declaration in a plugin whose [tool.qgis-sdk] hosts does not declare desktop: @toolbar, @menu or @shortcut, a Dialog/Dock surface, or a web view ([tool.qgis-sdk.web]). A server-only plugin loads that module and has nowhere to put a window; add desktop to hosts, or keep the declaration in a module only the Desktop host loads. |
HOST002 |
A server declaration in a plugin whose [tool.qgis-sdk] hosts does not declare server: @server.filter, @server.service or @server.access_control. The mirror of HOST001 - a Desktop plugin that imports a module of server hooks has nothing that will ever call them; add server to hosts, or keep the hooks in a module only the server host loads. Only qgis_sdk.server counts: filter and service are ordinary words, and a decorator from anywhere else is not a declaration this host recognises. |
HOST003 (warning) |
hosts declares server, but the QGIS this interpreter can see was built without QGIS Server, so qgis.server is absent. Nothing is wrong with the project - a developer machine without server bindings is normal - but qgis-sdk test --target server refuses the target here (by name, before any test runs) and a deployed server needs a build with WITH_SERVER=ON. conda-forge builds QGIS without it. |
SYNTAX001 |
A file does not parse. |
VERSION001 |
pyproject.toml pins qgis-sdk-runtime (or the qgis-sdk kit, which installs the runtime at the same number) in project.dependencies, an extra or a dependency group, and no version the pin admits lies in the runtime range this qgis-sdk-tools release was tested with (its own qgis-sdk-runtime>=X,<Y requirement). Release segments only; != is ignored; a specifier the arithmetic does not know is never reported. Skipped when the range cannot be read. |
VERSION002 |
A TypeScript file starting with // Generated by qgis-sdk. Do not edit. declares a PLUGIN_DESCRIPTION_SCHEMA_VERSION or HOST_SCHEMA_VERSION other than the one this release generates; regenerate with qgis-sdk bridge generate. node_modules is never read. |
DEP001 |
An import root is unknown: neither the standard library, QGIS, the SDK runtime, the plugin itself, a declared vendor distribution nor a declared external provides it. The ZIP cannot carry it, so the plugin would fail on a clean QGIS; the remediation names both exits (vendor it, or declare it external). qgis_sdk_tools is unknown with its own message - the tooling never ships. |
DEP002 |
qgis_sdk.testing is imported; the fakes are for the project’s tests and a built plugin never carries them (decision-35). |
DEP003 |
A [tool.qgis-sdk.build] vendor distribution ships a compiled extension (.so, .pyd, .dylib); a compiled package cannot be vendored, so it is an external with a probe instead. |
DEP004 |
A declared vendor distribution is not installed in the environment check runs in, or one of its requirements is neither vendored, declared external, nor something QGIS ships. The finding names the missing distribution. |
A module that must import QGIS when it loads (a widget module, a native
adapter of your own) is declared native so QGIS001/ARCH001 do not apply:
list dotted module globs under [tool.qgis-sdk.check] native-modules in the
project’s pyproject.toml, or pass --native for one run. Tooling trees
(.venv, node_modules, .pixi, build, dist, caches) are never checked.