Add QGIS 4.x (Qt6) support while keeping QGIS 3.22+ compatibility - #99
Conversation
QGIS 4.0 is Qt6/PyQt6-based, and the qgis.PyQt compatibility layer no longer allows unscoped Qt enum access or removed Qt5 APIs. Update the plugin so a single codebase runs on QGIS 3.22 through 4.x: - Use fully scoped Qt enums everywhere (e.g. Qt.ItemDataRole.UserRole, Qt.PenStyle.NoPen, QPalette.ColorRole.Base), which work identically on PyQt5 and PyQt6. QGIS API enums are unchanged, as the QGIS 4 bindings still support unscoped access for them. - Replace Qt5-only APIs: QFontMetrics.width() -> horizontalAdvance(), dialog exec_() -> exec(), and import QSvgWidget from QtSvgWidgets on Qt6 builds (with fallback for QGIS 4 releases that don't wrap it). - Replace the one direct PyQt5 import with qgis.PyQt. - metadata.txt: add qgisMaximumVersion=4.99 so the plugin is marked as QGIS 4 ready, and bump version to 1.1.0. - Make the heatmap FSL conversion test accept Qt6's slightly different gradient colour interpolation rounding. - Test harness fixes for newer QGIS/Python: create the QgsApplication before test discovery (widgets created at import time crash on Qt6 without an application), pass an explicit top level directory to unittest discovery, and add a run_tests_and_exit() entry point that reports failures through the exit code without running interpreter teardown (exiting a headless QgsApplication can crash on Qt6). - CI: test against QGIS 3.22, 3.34, 3.40 LTR, 3.44 LTR and 4.0 docker images, running the suite headless with QT_QPA_PLATFORM=offscreen (the qgis_testrunner.sh harness no longer executes scripts on the newer images). Verified: full test suite passes in qgis/qgis:release-3_22, ltr (3.44) and 4.0 (4.0.3, Qt6) docker images; flake8/pycodestyle/pylint clean. https://claude.ai/code/session_01LCTq6nNRJEWyBGQ1sjipHw
…ions The newer qgis/qgis images (3.40/3.44/4.0) exit immediately when run detached without a TTY, so pass -t to docker run. Also update the lint workflow off actions/setup-python@v1 with Python 3.9, which is no longer available on ubuntu-latest runners. https://claude.ai/code/session_01LCTq6nNRJEWyBGQ1sjipHw
Qt.GlobalColor members are lowercase so the enum scoping sweep missed them, and unscoped Qt.transparent raises AttributeError on PyQt6. The failure only occurs in paint paths (dialog header and recent map thumbnails), which unit tests don't reach, and repeated paint errors make the Add to Felt dialog unusable on QGIS 4. Verified by rendering the dialog and thumbnail compositing in both QGIS 4.0 (Qt 6.9.2) and QGIS 3.44 (Qt 5.15.17) containers. https://claude.ai/code/session_01LCTq6nNRJEWyBGQ1sjipHw
The multipart upload body was built with QByteArray.append(str), which PyQt6 rejects with TypeError, so every map upload failed on QGIS 4. Build the body as Python bytes instead and wrap it in a QByteArray at the end, which behaves identically on PyQt5. Add a unit test covering create_upload_file_request so this path is exercised by CI without network access. https://claude.ai/code/session_01LCTq6nNRJEWyBGQ1sjipHw
Test plan part 1: 3.22+ compatibility@samhashemi went through the first part since i had 3.4... installed already. to finish this test i need to try the raster styling step again with the last released plugin to make sure that it isn't a regression - see step 5 below. then i'll do part 2 with the newer version of QGIS! Testing with commit f62f2c1 zip download as the latest. On QGIS 3.x
|
|
@makella thanks for the thorough run-through! On the step 5 raster question — it's not a regression from this PR, so no need to re-test with the previous plugin release unless you want to confirm independently. To verify, I ran an A/B comparison inside a QGIS 3.44 container: the same DEM raster styled with each supported renderer (singleband pseudocolor interpolated/discrete/exact, singleband gray, paletted), converted to FSL by this branch and by What likely happened instead: the plugin only converts single-band renderers to Felt color-ramp styling. If a raster uses anything else (multiband RGB, hillshade, contrast-enhancement-only defaults, etc.), https://claude.ai/code/session_01LCTq6nNRJEWyBGQ1sjipHw Generated by Claude Code |
Test plan part 2: QGIS 4.0+
|
makella
left a comment
There was a problem hiding this comment.
ran through test plan on qgis 3.42.1 and 4.0.3 both check out and pass. as noted in the comments, there may be a raster rendering fix and/or bug if symbolized with singleband pseudocolor but that is outside the scope of this PR.


















Manual testing plan — ✅ complete on both QGIS 3.x and QGIS 4
Verified on a QGIS 3.4x install (Qt5) and a QGIS 4.0+ install (Qt6), with the
f62f2c1build:mainand this branch produce byte-identical raster FSL for all supported renderer types (details). Pre-existing behavior, to be filed as a separate issue.Earlier manual testing on QGIS 4 caught two Qt6-only bugs the unit suite can't reach — an endless repaint loop when opening the dialog (
Qt.transparent, fixed in 84fea26) and every upload failing (QByteArray.append(str), fixed in f62f2c1). A follow-up audit (static sweep for all known PyQt6 behavior changes + scripted render/click probes of every GUI component inside a QGIS 4.0.3 container) found no further issues, and a regression test now covers the upload request builder.Original step-by-step test plan (for future releases)
Plugins → Manage and Install Plugins → Install from ZIP. Loads with no Python errors, not flagged incompatible.Web → Add to Feltmenu and theShare Map to Felttoolbar button appear.Add to Felt. Progress bar advances, upload completes,Open Mapopens the map in the browser.Summary
QGIS 4.0 is Qt6/PyQt6-based and breaks plugins that use unscoped Qt enums or removed Qt5 APIs. This PR updates the plugin so a single codebase runs on everything from QGIS 3.22 through QGIS 4.x, and marks it "QGIS 4 ready" in the plugin repository.
Code changes
Qt.UserRole→Qt.ItemDataRole.UserRole,Qt.NoPen→Qt.PenStyle.NoPen,QDialogButtonBox.Ok→QDialogButtonBox.StandardButton.Ok, etc. — including the lowercaseQt.GlobalColormembers (Qt.transparent→Qt.GlobalColor.transparent) used in paint paths. The scoped forms work identically on PyQt5 and PyQt6. QGIS API enums (QgsUnitTypes.RenderPoints,QgsWkbTypes.Point, …) are unchanged — verified by introspection in a QGIS 4.0.3 container that the QGIS 4 bindings still support unscoped access for those.QFontMetrics.width()→horizontalAdvance(), dialogexec_()→exec(),QSvgWidgetimported fromQtSvgWidgetson Qt6 builds (with a fallback for QGIS 4.0.x, which doesn't wrap that module inqgis.PyQtyet), the one directPyQt5import now goes throughqgis.PyQt, and the multipart upload body is built asbytesinstead ofQByteArray.append(str), which PyQt6 rejects.qgisMaximumVersion=4.99(what lists the plugin as QGIS 4 compatible — the oldsupportsQt6flag is deprecated) and bumped the version to 1.1.0.Test/CI changes
create_upload_file_request(the multipart upload body builder) — previously only covered behind a live auth token, which is how the Qt6 upload bug slipped past CI.#c3c3c3vs#c4c4c4), and the converter output is correct on both.test_suite.pynow creates theQgsApplicationbefore test discovery (on Qt6, widgets created at module import crash without an application instance), passes an explicittop_level_dir(required on newer Python for the test modules' relative imports), and adds arun_tests_and_exit()entry point that reports failures via exit code without running interpreter teardown (exiting a headlessQgsApplicationcan segfault on Qt6, which would mask the result).QT_QPA_PLATFORM=offscreenbecause the newer official Docker images never executeqgis --codescripts non-interactively (the oldqgis_testrunner.shflow hangs on them).Verification
Full suite (69 tests) passes with the exact CI command in
qgis/qgis:release-3_22(Qt5),ltr/3.44 (Qt5) and4.0/4.0.3 (Qt6) Docker images; flake8, pycodestyle and pylint clean/at baseline. CI is green across the whole matrix.Note: this overlaps with the
update-for-qgis4branch from March — this PR additionally keeps QGIS 3.22 compatibility, so users on older QGIS continue to get updates.https://claude.ai/code/session_01LCTq6nNRJEWyBGQ1sjipHw
Generated by Claude Code