[{"data":1,"prerenderedAt":2018},["ShallowReactive",2],{"doc:\u002Fqgis-plugin-development\u002Fplugin-boilerplate-structure\u002Fport-qgis-plugin-to-qgis-4-and-qt6":3},{"id":4,"title":5,"body":6,"description":2007,"extension":2008,"meta":2009,"navigation":246,"path":2014,"seo":2015,"stem":2016,"__hash__":2017},"docs\u002Fqgis-plugin-development\u002Fplugin-boilerplate-structure\u002Fport-qgis-plugin-to-qgis-4-and-qt6\u002Findex.md","Port a QGIS Plugin to QGIS 4 and Qt6",{"type":7,"value":8,"toc":1994},"minimark",[9,13,17,31,161,166,199,203,206,943,963,967,977,1121,1203,1208,1212,1215,1456,1518,1522,1536,1595,1642,1663,1667,1670,1803,1828,1832,1838,1842,1893,1897,1918,1922,1928,1941,1951,1957,1961,1990],[10,11,5],"h1",{"id":12},"port-a-qgis-plugin-to-qgis-4-and-qt6",[14,15,16],"p",{},"The QGIS 4 series moves the application from Qt5 and PyQt5 to Qt6 and PyQt6. For plugin authors the good news is that the QGIS API itself barely changes between late 3.x and 4.0 — the deprecations that were removed had been flagged for years. The work is almost all on the Qt side: PyQt6 is stricter about enums, drops a handful of legacy names, moves a few classes between modules, and no longer ships the resource compiler many plugins relied on. A plugin written carefully can run unchanged on 3.40 LTR, 3.44 LTR and QGIS 4 from one codebase.",[14,18,19,20,25,26,30],{},"This recipe belongs to ",[21,22,24],"a",{"href":23},"\u002Fqgis-plugin-development\u002Fplugin-boilerplate-structure\u002F","QGIS Plugin Boilerplate & Structure",". It audits a plugin for Qt5-only code, applies the automated and manual fixes, updates ",[27,28,29],"code",{},"metadata.txt"," so the plugin repository offers it to QGIS 4 users, and sets up testing against both generations.",[14,32,33],{},[34,35,40,44,48,55,64,74,80,85,90,98,104,108,111,115,118,121,125,128,131,135,138,141,145,148,151,155,158],"svg",{"viewBox":36,"role":37,"ariaLabel":38,"xmlns":39},"0 0 760 300","img","What changes between QGIS 3 with Qt5 and QGIS 4 with Qt6 for a plugin: the QGIS API is largely the same, while Qt changes include scoped enums, exec instead of exec_, QAction moving to QtGui, QRegExp removal, no pyrcc resource compiler, and QVariant types replaced by QMetaType","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[41,42,43],"title",{},"Where the porting work is",[45,46,47],"desc",{},"Two columns compare QGIS 3 on Qt5 with QGIS 4 on Qt6. Rows: enums, unscoped Qt.AlignLeft accepted versus only Qt.AlignmentFlag.AlignLeft; dialogs, exec_ versus exec; actions, QAction in QtWidgets versus QtGui; regular expressions, QRegExp versus QRegularExpression; resources, pyrcc5 compiled resources.py versus icons loaded from files; field types, QVariant.String versus QMetaType.Type.QString. A note says the QGIS API calls themselves mostly work on both when written with scoped QGIS enums.",[49,50],"rect",{"x":51,"y":51,"width":52,"height":53,"fill":54},"0","760","300","#f6f3ea",[56,57,63],"text",{"x":58,"y":59,"style":60,"fill":61,"textAnchor":62},"380","28","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","Mostly Qt changes, rarely QGIS ones",[49,65],{"x":66,"y":67,"width":68,"height":69,"rx":70,"fill":71,"stroke":72,"style":73},"24","44","712","236","10","#fffdf7","#59645f","stroke-width:2",[56,75,79],{"x":76,"y":77,"style":78,"fill":61,"textAnchor":62},"130","70","text-anchor:middle;font-size:10.5px;font-weight:bold;font-family:sans-serif","area",[56,81,84],{"x":82,"y":77,"style":78,"fill":83,"textAnchor":62},"350","#b45309","Qt5 habit",[56,86,89],{"x":87,"y":77,"style":78,"fill":88,"textAnchor":62},"590","#166534","works on both",[91,92],"line",{"x1":93,"y1":94,"x2":95,"y2":94,"stroke":96,"style":97},"40","80","720","#d9d3c4","stroke-width:1.5",[56,99,103],{"x":76,"y":100,"style":101,"fill":102,"textAnchor":62},"104","text-anchor:middle;font-size:10px;font-family:sans-serif","#2f3b35","enums",[56,105,107],{"x":82,"y":100,"style":106,"fill":102,"textAnchor":62},"text-anchor:middle;font-size:10px;font-family:monospace","Qt.AlignLeft",[56,109,110],{"x":87,"y":100,"style":106,"fill":102,"textAnchor":62},"Qt.AlignmentFlag.AlignLeft",[56,112,114],{"x":76,"y":113,"style":101,"fill":102,"textAnchor":62},"134","dialogs",[56,116,117],{"x":82,"y":113,"style":106,"fill":102,"textAnchor":62},"dlg.exec_()",[56,119,120],{"x":87,"y":113,"style":106,"fill":102,"textAnchor":62},"dlg.exec()",[56,122,124],{"x":76,"y":123,"style":101,"fill":102,"textAnchor":62},"164","actions",[56,126,127],{"x":82,"y":123,"style":106,"fill":102,"textAnchor":62},"QtWidgets.QAction",[56,129,130],{"x":87,"y":123,"style":106,"fill":102,"textAnchor":62},"QtGui.QAction (with fallback)",[56,132,134],{"x":76,"y":133,"style":101,"fill":102,"textAnchor":62},"194","regex",[56,136,137],{"x":82,"y":133,"style":106,"fill":102,"textAnchor":62},"QRegExp",[56,139,140],{"x":87,"y":133,"style":106,"fill":102,"textAnchor":62},"QRegularExpression",[56,142,144],{"x":76,"y":143,"style":101,"fill":102,"textAnchor":62},"224","resources",[56,146,147],{"x":82,"y":143,"style":106,"fill":102,"textAnchor":62},"pyrcc5 resources.py",[56,149,150],{"x":87,"y":143,"style":106,"fill":102,"textAnchor":62},"icons loaded by file path",[56,152,154],{"x":76,"y":153,"style":101,"fill":102,"textAnchor":62},"254","field types",[56,156,157],{"x":82,"y":153,"style":106,"fill":102,"textAnchor":62},"QVariant.String",[56,159,160],{"x":87,"y":153,"style":106,"fill":102,"textAnchor":62},"QMetaType.Type.QString",[162,163,165],"h2",{"id":164},"prerequisites","Prerequisites",[167,168,169,173,188],"ul",{},[170,171,172],"li",{},"The plugin's source in version control, on a branch for the port.",[170,174,175,179,180,183,184,187],{},[176,177,178],"strong",{},"QGIS 3.40 LTR"," or ",[176,181,182],{},"3.44 LTR"," to keep supporting, and a QGIS 4 build to test against. Pick a minimum version of 3.38 or later if you can: it is the first release that accepts every Qt6-compatible spelling used below, including ",[27,185,186],{},"QMetaType"," field types.",[170,189,190,191,194,195,198],{},"Imports that already go through ",[27,192,193],{},"qgis.PyQt"," rather than ",[27,196,197],{},"PyQt5"," directly. If they do not, that is the first fix.",[162,200,202],{"id":201},"audit-the-plugin","Audit the plugin",[14,204,205],{},"Before changing anything, find out how much there is to change. A short scan for the Qt5-only patterns gives a realistic size for the job and a checklist to work through.",[207,208,213],"pre",{"className":209,"code":210,"language":211,"meta":212,"style":212},"language-python shiki shiki-themes github-dark","import re\nfrom pathlib import Path\n\nPATTERNS = {\n    \"direct PyQt5 import\": r\"\\bfrom PyQt5\\b|\\bimport PyQt5\\b\",\n    \"exec_()\": r\"\\.exec_\\(\",\n    \"QRegExp\": r\"\\bQRegExp\\b\",\n    \"QVariant field type\": r\"QVariant\\.(String|Int|Double|LongLong|Bool|Date|DateTime)\\b\",\n    \"unscoped Qt enum\": r\"\\bQt\\.(Align\\w+|Checked|Unchecked|UserRole|DisplayRole|LeftButton|RightButton|Key_\\w+)\\b\",\n    \"unscoped dialog button\": r\"\\bQ(MessageBox|DialogButtonBox)\\.(Yes|No|Ok|Cancel|Save|Close)\\b\",\n    \"QAction from QtWidgets\": r\"QtWidgets import[^\\n]*\\bQAction\\b\",\n    \"compiled resources\": r\"\\bresources_rc\\b|\\bimport resources\\b|:\u002Fplugins\u002F\",\n    \"QDesktopWidget\": r\"\\bQDesktopWidget\\b\",\n    \"deprecated QgsWkbTypes enum\": r\"QgsWkbTypes\\.(Point|LineString|Polygon|Multi\\w+)\\b\",\n}\n\nroot = Path(\"\u002Fhome\u002Fdev\u002Fplugins\u002Fasset_tools\")\ntotals = {name: 0 for name in PATTERNS}\nfor path in root.rglob(\"*.py\"):\n    text = path.read_text(encoding=\"utf-8\", errors=\"ignore\")\n    for name, pattern in PATTERNS.items():\n        hits = len(re.findall(pattern, text))\n        if hits:\n            totals[name] += hits\n            print(f\"{path.relative_to(root)}: {name} ×{hits}\")\n\nprint(\"\\nsummary:\", {k: v for k, v in totals.items() if v})\n","python","",[27,214,215,227,241,248,261,301,327,349,409,483,551,592,629,652,699,705,710,728,755,775,808,824,838,847,859,904,909],{"__ignoreMap":212},[216,217,219,223],"span",{"class":91,"line":218},1,[216,220,222],{"class":221},"snl16","import",[216,224,226],{"class":225},"s95oV"," re\n",[216,228,230,233,236,238],{"class":91,"line":229},2,[216,231,232],{"class":221},"from",[216,234,235],{"class":225}," pathlib ",[216,237,222],{"class":221},[216,239,240],{"class":225}," Path\n",[216,242,244],{"class":91,"line":243},3,[216,245,247],{"emptyLinePlaceholder":246},true,"\n",[216,249,251,255,258],{"class":91,"line":250},4,[216,252,254],{"class":253},"sDLfK","PATTERNS",[216,256,257],{"class":221}," =",[216,259,260],{"class":225}," {\n",[216,262,264,268,271,274,277,280,284,286,289,291,294,296,298],{"class":91,"line":263},5,[216,265,267],{"class":266},"sU2Wk","    \"direct PyQt5 import\"",[216,269,270],{"class":225},": ",[216,272,273],{"class":221},"r",[216,275,276],{"class":266},"\"",[216,278,279],{"class":253},"\\b",[216,281,283],{"class":282},"sns5M","from PyQt5",[216,285,279],{"class":253},[216,287,288],{"class":221},"|",[216,290,279],{"class":253},[216,292,293],{"class":282},"import PyQt5",[216,295,279],{"class":253},[216,297,276],{"class":266},[216,299,300],{"class":225},",\n",[216,302,304,307,309,311,313,317,320,323,325],{"class":91,"line":303},6,[216,305,306],{"class":266},"    \"exec_()\"",[216,308,270],{"class":225},[216,310,273],{"class":221},[216,312,276],{"class":266},[216,314,316],{"class":315},"sRjNt","\\.",[216,318,319],{"class":282},"exec_",[216,321,322],{"class":315},"\\(",[216,324,276],{"class":266},[216,326,300],{"class":225},[216,328,330,333,335,337,339,341,343,345,347],{"class":91,"line":329},7,[216,331,332],{"class":266},"    \"QRegExp\"",[216,334,270],{"class":225},[216,336,273],{"class":221},[216,338,276],{"class":266},[216,340,279],{"class":253},[216,342,137],{"class":282},[216,344,279],{"class":253},[216,346,276],{"class":266},[216,348,300],{"class":225},[216,350,352,355,357,359,361,364,366,369,372,374,377,379,382,384,387,389,392,394,397,399,402,405,407],{"class":91,"line":351},8,[216,353,354],{"class":266},"    \"QVariant field type\"",[216,356,270],{"class":225},[216,358,273],{"class":221},[216,360,276],{"class":266},[216,362,363],{"class":282},"QVariant",[216,365,316],{"class":315},[216,367,368],{"class":253},"(",[216,370,371],{"class":282},"String",[216,373,288],{"class":221},[216,375,376],{"class":282},"Int",[216,378,288],{"class":221},[216,380,381],{"class":282},"Double",[216,383,288],{"class":221},[216,385,386],{"class":282},"LongLong",[216,388,288],{"class":221},[216,390,391],{"class":282},"Bool",[216,393,288],{"class":221},[216,395,396],{"class":282},"Date",[216,398,288],{"class":221},[216,400,401],{"class":282},"DateTime",[216,403,404],{"class":253},")\\b",[216,406,276],{"class":266},[216,408,300],{"class":225},[216,410,412,415,417,419,421,423,426,428,430,433,436,439,442,444,447,449,452,454,457,459,462,464,467,469,472,474,477,479,481],{"class":91,"line":411},9,[216,413,414],{"class":266},"    \"unscoped Qt enum\"",[216,416,270],{"class":225},[216,418,273],{"class":221},[216,420,276],{"class":266},[216,422,279],{"class":253},[216,424,425],{"class":282},"Qt",[216,427,316],{"class":315},[216,429,368],{"class":253},[216,431,432],{"class":282},"Align",[216,434,435],{"class":253},"\\w",[216,437,438],{"class":221},"+|",[216,440,441],{"class":282},"Checked",[216,443,288],{"class":221},[216,445,446],{"class":282},"Unchecked",[216,448,288],{"class":221},[216,450,451],{"class":282},"UserRole",[216,453,288],{"class":221},[216,455,456],{"class":282},"DisplayRole",[216,458,288],{"class":221},[216,460,461],{"class":282},"LeftButton",[216,463,288],{"class":221},[216,465,466],{"class":282},"RightButton",[216,468,288],{"class":221},[216,470,471],{"class":282},"Key_",[216,473,435],{"class":253},[216,475,476],{"class":221},"+",[216,478,404],{"class":253},[216,480,276],{"class":266},[216,482,300],{"class":225},[216,484,486,489,491,493,495,497,500,502,505,507,510,513,515,517,520,522,525,527,530,532,535,537,540,542,545,547,549],{"class":91,"line":485},10,[216,487,488],{"class":266},"    \"unscoped dialog button\"",[216,490,270],{"class":225},[216,492,273],{"class":221},[216,494,276],{"class":266},[216,496,279],{"class":253},[216,498,499],{"class":282},"Q",[216,501,368],{"class":253},[216,503,504],{"class":282},"MessageBox",[216,506,288],{"class":221},[216,508,509],{"class":282},"DialogButtonBox",[216,511,512],{"class":253},")",[216,514,316],{"class":315},[216,516,368],{"class":253},[216,518,519],{"class":282},"Yes",[216,521,288],{"class":221},[216,523,524],{"class":282},"No",[216,526,288],{"class":221},[216,528,529],{"class":282},"Ok",[216,531,288],{"class":221},[216,533,534],{"class":282},"Cancel",[216,536,288],{"class":221},[216,538,539],{"class":282},"Save",[216,541,288],{"class":221},[216,543,544],{"class":282},"Close",[216,546,404],{"class":253},[216,548,276],{"class":266},[216,550,300],{"class":225},[216,552,554,557,559,561,563,566,569,572,575,578,581,583,586,588,590],{"class":91,"line":553},11,[216,555,556],{"class":266},"    \"QAction from QtWidgets\"",[216,558,270],{"class":225},[216,560,273],{"class":221},[216,562,276],{"class":266},[216,564,565],{"class":282},"QtWidgets import",[216,567,568],{"class":253},"[",[216,570,571],{"class":221},"^",[216,573,574],{"class":315},"\\n",[216,576,577],{"class":253},"]",[216,579,580],{"class":221},"*",[216,582,279],{"class":253},[216,584,585],{"class":282},"QAction",[216,587,279],{"class":253},[216,589,276],{"class":266},[216,591,300],{"class":225},[216,593,595,598,600,602,604,606,609,611,613,615,618,620,622,625,627],{"class":91,"line":594},12,[216,596,597],{"class":266},"    \"compiled resources\"",[216,599,270],{"class":225},[216,601,273],{"class":221},[216,603,276],{"class":266},[216,605,279],{"class":253},[216,607,608],{"class":282},"resources_rc",[216,610,279],{"class":253},[216,612,288],{"class":221},[216,614,279],{"class":253},[216,616,617],{"class":282},"import resources",[216,619,279],{"class":253},[216,621,288],{"class":221},[216,623,624],{"class":282},":\u002Fplugins\u002F",[216,626,276],{"class":266},[216,628,300],{"class":225},[216,630,632,635,637,639,641,643,646,648,650],{"class":91,"line":631},13,[216,633,634],{"class":266},"    \"QDesktopWidget\"",[216,636,270],{"class":225},[216,638,273],{"class":221},[216,640,276],{"class":266},[216,642,279],{"class":253},[216,644,645],{"class":282},"QDesktopWidget",[216,647,279],{"class":253},[216,649,276],{"class":266},[216,651,300],{"class":225},[216,653,655,658,660,662,664,667,669,671,674,676,679,681,684,686,689,691,693,695,697],{"class":91,"line":654},14,[216,656,657],{"class":266},"    \"deprecated QgsWkbTypes enum\"",[216,659,270],{"class":225},[216,661,273],{"class":221},[216,663,276],{"class":266},[216,665,666],{"class":282},"QgsWkbTypes",[216,668,316],{"class":315},[216,670,368],{"class":253},[216,672,673],{"class":282},"Point",[216,675,288],{"class":221},[216,677,678],{"class":282},"LineString",[216,680,288],{"class":221},[216,682,683],{"class":282},"Polygon",[216,685,288],{"class":221},[216,687,688],{"class":282},"Multi",[216,690,435],{"class":253},[216,692,476],{"class":221},[216,694,404],{"class":253},[216,696,276],{"class":266},[216,698,300],{"class":225},[216,700,702],{"class":91,"line":701},15,[216,703,704],{"class":225},"}\n",[216,706,708],{"class":91,"line":707},16,[216,709,247],{"emptyLinePlaceholder":246},[216,711,713,716,719,722,725],{"class":91,"line":712},17,[216,714,715],{"class":225},"root ",[216,717,718],{"class":221},"=",[216,720,721],{"class":225}," Path(",[216,723,724],{"class":266},"\"\u002Fhome\u002Fdev\u002Fplugins\u002Fasset_tools\"",[216,726,727],{"class":225},")\n",[216,729,731,734,736,739,741,744,747,750,753],{"class":91,"line":730},18,[216,732,733],{"class":225},"totals ",[216,735,718],{"class":221},[216,737,738],{"class":225}," {name: ",[216,740,51],{"class":253},[216,742,743],{"class":221}," for",[216,745,746],{"class":225}," name ",[216,748,749],{"class":221},"in",[216,751,752],{"class":253}," PATTERNS",[216,754,704],{"class":225},[216,756,758,761,764,766,769,772],{"class":91,"line":757},19,[216,759,760],{"class":221},"for",[216,762,763],{"class":225}," path ",[216,765,749],{"class":221},[216,767,768],{"class":225}," root.rglob(",[216,770,771],{"class":266},"\"*.py\"",[216,773,774],{"class":225},"):\n",[216,776,778,781,783,786,790,792,795,798,801,803,806],{"class":91,"line":777},20,[216,779,780],{"class":225},"    text ",[216,782,718],{"class":221},[216,784,785],{"class":225}," path.read_text(",[216,787,789],{"class":788},"s9osk","encoding",[216,791,718],{"class":221},[216,793,794],{"class":266},"\"utf-8\"",[216,796,797],{"class":225},", ",[216,799,800],{"class":788},"errors",[216,802,718],{"class":221},[216,804,805],{"class":266},"\"ignore\"",[216,807,727],{"class":225},[216,809,811,814,817,819,821],{"class":91,"line":810},21,[216,812,813],{"class":221},"    for",[216,815,816],{"class":225}," name, pattern ",[216,818,749],{"class":221},[216,820,752],{"class":253},[216,822,823],{"class":225},".items():\n",[216,825,827,830,832,835],{"class":91,"line":826},22,[216,828,829],{"class":225},"        hits ",[216,831,718],{"class":221},[216,833,834],{"class":253}," len",[216,836,837],{"class":225},"(re.findall(pattern, text))\n",[216,839,841,844],{"class":91,"line":840},23,[216,842,843],{"class":221},"        if",[216,845,846],{"class":225}," hits:\n",[216,848,850,853,856],{"class":91,"line":849},24,[216,851,852],{"class":225},"            totals[name] ",[216,854,855],{"class":221},"+=",[216,857,858],{"class":225}," hits\n",[216,860,862,865,867,870,872,875,878,881,883,885,888,890,893,895,898,900,902],{"class":91,"line":861},25,[216,863,864],{"class":253},"            print",[216,866,368],{"class":225},[216,868,869],{"class":221},"f",[216,871,276],{"class":266},[216,873,874],{"class":253},"{",[216,876,877],{"class":225},"path.relative_to(root)",[216,879,880],{"class":253},"}",[216,882,270],{"class":266},[216,884,874],{"class":253},[216,886,887],{"class":225},"name",[216,889,880],{"class":253},[216,891,892],{"class":266}," ×",[216,894,874],{"class":253},[216,896,897],{"class":225},"hits",[216,899,880],{"class":253},[216,901,276],{"class":266},[216,903,727],{"class":225},[216,905,907],{"class":91,"line":906},26,[216,908,247],{"emptyLinePlaceholder":246},[216,910,912,915,917,919,921,924,927,929,932,934,937,940],{"class":91,"line":911},27,[216,913,914],{"class":253},"print",[216,916,368],{"class":225},[216,918,276],{"class":266},[216,920,574],{"class":253},[216,922,923],{"class":266},"summary:\"",[216,925,926],{"class":225},", {k: v ",[216,928,760],{"class":221},[216,930,931],{"class":225}," k, v ",[216,933,749],{"class":221},[216,935,936],{"class":225}," totals.items() ",[216,938,939],{"class":221},"if",[216,941,942],{"class":225}," v})\n",[14,944,945,948,949,951,952,954,955,958,959,962],{},[176,946,947],{},"Breakdown:"," The patterns are deliberately simple and will produce some false positives — a comment mentioning ",[27,950,137],{},", a string containing ",[27,953,624],{}," — which is fine for an audit whose job is to point at files, not to fix them. Unscoped enums are the largest category in most plugins, and the pattern list covers only the most common ones; the migration script in the next section finds the rest. ",[27,956,957],{},".ui"," files are not scanned because Qt Designer files are XML that ",[27,960,961],{},"uic"," translates for both Qt versions, though custom widget plugins inside them still need checking.",[162,964,966],{"id":965},"apply-the-automated-fixes","Apply the automated fixes",[14,968,969,970,973,974,976],{},"The QGIS source repository ships a migration script, ",[27,971,972],{},"scripts\u002Fpyqt5_to_pyqt6\u002Fpyqt5_to_pyqt6.py",", that rewrites the mechanical changes — enum scoping, ",[27,975,319],{},", moved classes — using an AST-based pass, so it understands context in a way regular expressions cannot.",[14,978,979],{},[34,980,983,986,989,992,1008,1012,1018,1023,1028,1034,1039,1043,1046,1050,1054,1058,1061,1065,1068,1072,1075,1079,1084,1088,1091,1095,1101,1105,1107,1113,1118],{"viewBox":981,"role":37,"ariaLabel":982,"xmlns":39},"0 0 760 256","A porting workflow: audit, run the migration script on a branch, review the diff, apply manual fixes the script cannot make, run tests on both QGIS 3 LTR and QGIS 4, then update metadata and release",[41,984,985],{},"Port in a reviewable sequence",[45,987,988],{},"Six steps. Audit the plugin with a pattern scan. Run the QGIS migration script on a clean branch so every change is a diff. Review the diff by hand, because scoping an enum on the wrong class breaks at runtime. Apply manual fixes: resources, QVariant field types, removed classes. Run the test suite on QGIS 3.40 or 3.44 and on QGIS 4. Update metadata.txt and release.",[49,990],{"x":51,"y":51,"width":52,"height":991,"fill":54},"256",[993,994,995],"defs",{},[996,997,1004],"marker",{"id":998,"viewBox":999,"refX":1000,"refY":1001,"markerWidth":1002,"markerHeight":1002,"orient":1003},"q4StepArrow","0 0 10 10","8","5","7","auto-start-reverse",[1005,1006],"path",{"d":1007,"fill":102},"M0 0 L10 5 L0 10 z",[56,1009,1011],{"x":58,"y":1010,"style":60,"fill":61,"textAnchor":62},"26","Every change lands as a diff someone reviews",[49,1013],{"x":1014,"y":1015,"width":1016,"height":1017,"rx":70,"fill":71,"stroke":72,"style":73},"16","60","110","90",[56,1019,1022],{"x":1020,"y":1021,"style":78,"fill":61,"textAnchor":62},"71","96","1 audit",[56,1024,1027],{"x":1020,"y":1025,"style":1026,"fill":102,"textAnchor":62},"118","text-anchor:middle;font-size:9.5px;font-family:sans-serif","size the job",[91,1029],{"x1":1030,"y1":1031,"x2":1032,"y2":1031,"stroke":102,"style":1033},"126","105","136","stroke-width:1.6;marker-end:url(#q4StepArrow)",[49,1035],{"x":1036,"y":1015,"width":1016,"height":1017,"rx":70,"fill":1037,"stroke":1038,"style":73},"142","#eef7f4","#0f766e",[56,1040,1042],{"x":1041,"y":1021,"style":78,"fill":1038,"textAnchor":62},"197","2 script",[56,1044,1045],{"x":1041,"y":1025,"style":1026,"fill":102,"textAnchor":62},"mechanical fixes",[91,1047],{"x1":1048,"y1":1031,"x2":1049,"y2":1031,"stroke":102,"style":1033},"252","262",[49,1051],{"x":1052,"y":1015,"width":1016,"height":1017,"rx":70,"fill":1053,"stroke":83,"style":73},"268","#fdf2e2",[56,1055,1057],{"x":1056,"y":1021,"style":78,"fill":83,"textAnchor":62},"323","3 review",[56,1059,1060],{"x":1056,"y":1025,"style":1026,"fill":102,"textAnchor":62},"read the diff",[91,1062],{"x1":1063,"y1":1031,"x2":1064,"y2":1031,"stroke":102,"style":1033},"378","388",[49,1066],{"x":1067,"y":1015,"width":1016,"height":1017,"rx":70,"fill":1053,"stroke":83,"style":73},"394",[56,1069,1071],{"x":1070,"y":1021,"style":78,"fill":83,"textAnchor":62},"449","4 manual",[56,1073,1074],{"x":1070,"y":1025,"style":1026,"fill":102,"textAnchor":62},"resources, types",[91,1076],{"x1":1077,"y1":1031,"x2":1078,"y2":1031,"stroke":102,"style":1033},"504","514",[49,1080],{"x":1081,"y":1015,"width":1016,"height":1017,"rx":70,"fill":1082,"stroke":1083,"style":73},"520","#eff3ff","#2563eb",[56,1085,1087],{"x":1086,"y":1021,"style":78,"fill":1083,"textAnchor":62},"575","5 test",[56,1089,1090],{"x":1086,"y":1025,"style":1026,"fill":102,"textAnchor":62},"3.x LTR + 4.x",[91,1092],{"x1":1093,"y1":1031,"x2":1094,"y2":1031,"stroke":102,"style":1033},"630","640",[49,1096],{"x":1097,"y":1015,"width":1098,"height":1017,"rx":70,"fill":1099,"stroke":1100,"style":73},"646","98","#e8efe6","#15803d",[56,1102,1104],{"x":1103,"y":1021,"style":78,"fill":88,"textAnchor":62},"695","6 release",[56,1106,29],{"x":1103,"y":1025,"style":1026,"fill":102,"textAnchor":62},[49,1108],{"x":1109,"y":1110,"width":1081,"height":1111,"rx":70,"fill":71,"stroke":72,"style":1112},"120","182","52","stroke-width:1.8",[56,1114,1117],{"x":58,"y":1115,"style":1116,"fill":102,"textAnchor":62},"206","text-anchor:middle;font-size:10.5px;font-family:sans-serif","one codebase for both generations —",[56,1119,1120],{"x":58,"y":143,"style":1116,"fill":102,"textAnchor":62},"no qgis3\u002F and qgis4\u002F forks to maintain",[207,1122,1126],{"className":1123,"code":1124,"language":1125,"meta":212,"style":212},"language-bash shiki shiki-themes github-dark","git clone --depth 1 https:\u002F\u002Fgithub.com\u002Fqgis\u002FQGIS.git \u002Ftmp\u002Fqgis-src\ncd \u002Fhome\u002Fdev\u002Fplugins\u002Fasset_tools\ngit switch -c qt6-port\npython3 \u002Ftmp\u002Fqgis-src\u002Fscripts\u002Fpyqt5_to_pyqt6\u002Fpyqt5_to_pyqt6.py .\ngit diff --stat\ngit diff | less\n","bash",[27,1127,1128,1149,1157,1170,1181,1191],{"__ignoreMap":212},[216,1129,1130,1134,1137,1140,1143,1146],{"class":91,"line":218},[216,1131,1133],{"class":1132},"svObZ","git",[216,1135,1136],{"class":266}," clone",[216,1138,1139],{"class":253}," --depth",[216,1141,1142],{"class":253}," 1",[216,1144,1145],{"class":266}," https:\u002F\u002Fgithub.com\u002Fqgis\u002FQGIS.git",[216,1147,1148],{"class":266}," \u002Ftmp\u002Fqgis-src\n",[216,1150,1151,1154],{"class":91,"line":229},[216,1152,1153],{"class":253},"cd",[216,1155,1156],{"class":266}," \u002Fhome\u002Fdev\u002Fplugins\u002Fasset_tools\n",[216,1158,1159,1161,1164,1167],{"class":91,"line":243},[216,1160,1133],{"class":1132},[216,1162,1163],{"class":266}," switch",[216,1165,1166],{"class":253}," -c",[216,1168,1169],{"class":266}," qt6-port\n",[216,1171,1172,1175,1178],{"class":91,"line":250},[216,1173,1174],{"class":1132},"python3",[216,1176,1177],{"class":266}," \u002Ftmp\u002Fqgis-src\u002Fscripts\u002Fpyqt5_to_pyqt6\u002Fpyqt5_to_pyqt6.py",[216,1179,1180],{"class":266}," .\n",[216,1182,1183,1185,1188],{"class":91,"line":263},[216,1184,1133],{"class":1132},[216,1186,1187],{"class":266}," diff",[216,1189,1190],{"class":253}," --stat\n",[216,1192,1193,1195,1197,1200],{"class":91,"line":303},[216,1194,1133],{"class":1132},[216,1196,1187],{"class":266},[216,1198,1199],{"class":221}," |",[216,1201,1202],{"class":1132}," less\n",[14,1204,1205,1207],{},[176,1206,947],{}," Running the script on a clean branch turns every automated change into a reviewable diff, which matters because the script occasionally scopes an enum on the wrong class when a name is ambiguous — the kind of error that only fails when that line runs. The script's own dependencies are listed at the top of the file; install them in a virtual environment rather than QGIS's Python. Reviewing the diff also teaches the new spellings, which helps when writing new code. Commit the script's output as one commit and manual fixes as separate ones, so a later bisect can tell them apart.",[162,1209,1211],{"id":1210},"manual-fixes-the-script-cannot-make","Manual fixes the script cannot make",[14,1213,1214],{},"A few changes need judgement. The common ones, with forms that work on both Qt versions:",[207,1216,1218],{"className":209,"code":1217,"language":211,"meta":212,"style":212},"import os\nfrom qgis.PyQt.QtCore import QMetaType, QRegularExpression\nfrom qgis.PyQt.QtGui import QIcon, QRegularExpressionValidator\ntry:\n    from qgis.PyQt.QtGui import QAction\nexcept ImportError:\n    from qgis.PyQt.QtWidgets import QAction\nfrom qgis.core import QgsField\n\nPLUGIN_DIR = os.path.dirname(__file__)\n\ndef icon(name):\n    return QIcon(os.path.join(PLUGIN_DIR, \"icons\", name))\n\naction = QAction(icon(\"export.svg\"), \"Export to asset system\")\n\nasset_id_validator = QRegularExpressionValidator(QRegularExpression(r\"^[A-Z]{2}-\\d{5}$\"))\n\nnew_field = QgsField(\"inspected_on\", QMetaType.Type.QDate)\n\nresult = dialog.exec()\n",[27,1219,1220,1227,1239,1251,1259,1271,1281,1292,1304,1308,1323,1327,1338,1356,1360,1381,1385,1422,1426,1442,1446],{"__ignoreMap":212},[216,1221,1222,1224],{"class":91,"line":218},[216,1223,222],{"class":221},[216,1225,1226],{"class":225}," os\n",[216,1228,1229,1231,1234,1236],{"class":91,"line":229},[216,1230,232],{"class":221},[216,1232,1233],{"class":225}," qgis.PyQt.QtCore ",[216,1235,222],{"class":221},[216,1237,1238],{"class":225}," QMetaType, QRegularExpression\n",[216,1240,1241,1243,1246,1248],{"class":91,"line":243},[216,1242,232],{"class":221},[216,1244,1245],{"class":225}," qgis.PyQt.QtGui ",[216,1247,222],{"class":221},[216,1249,1250],{"class":225}," QIcon, QRegularExpressionValidator\n",[216,1252,1253,1256],{"class":91,"line":250},[216,1254,1255],{"class":221},"try",[216,1257,1258],{"class":225},":\n",[216,1260,1261,1264,1266,1268],{"class":91,"line":263},[216,1262,1263],{"class":221},"    from",[216,1265,1245],{"class":225},[216,1267,222],{"class":221},[216,1269,1270],{"class":225}," QAction\n",[216,1272,1273,1276,1279],{"class":91,"line":303},[216,1274,1275],{"class":221},"except",[216,1277,1278],{"class":253}," ImportError",[216,1280,1258],{"class":225},[216,1282,1283,1285,1288,1290],{"class":91,"line":329},[216,1284,1263],{"class":221},[216,1286,1287],{"class":225}," qgis.PyQt.QtWidgets ",[216,1289,222],{"class":221},[216,1291,1270],{"class":225},[216,1293,1294,1296,1299,1301],{"class":91,"line":351},[216,1295,232],{"class":221},[216,1297,1298],{"class":225}," qgis.core ",[216,1300,222],{"class":221},[216,1302,1303],{"class":225}," QgsField\n",[216,1305,1306],{"class":91,"line":411},[216,1307,247],{"emptyLinePlaceholder":246},[216,1309,1310,1313,1315,1318,1321],{"class":91,"line":485},[216,1311,1312],{"class":253},"PLUGIN_DIR",[216,1314,257],{"class":221},[216,1316,1317],{"class":225}," os.path.dirname(",[216,1319,1320],{"class":253},"__file__",[216,1322,727],{"class":225},[216,1324,1325],{"class":91,"line":553},[216,1326,247],{"emptyLinePlaceholder":246},[216,1328,1329,1332,1335],{"class":91,"line":594},[216,1330,1331],{"class":221},"def",[216,1333,1334],{"class":1132}," icon",[216,1336,1337],{"class":225},"(name):\n",[216,1339,1340,1343,1346,1348,1350,1353],{"class":91,"line":631},[216,1341,1342],{"class":221},"    return",[216,1344,1345],{"class":225}," QIcon(os.path.join(",[216,1347,1312],{"class":253},[216,1349,797],{"class":225},[216,1351,1352],{"class":266},"\"icons\"",[216,1354,1355],{"class":225},", name))\n",[216,1357,1358],{"class":91,"line":654},[216,1359,247],{"emptyLinePlaceholder":246},[216,1361,1362,1365,1367,1370,1373,1376,1379],{"class":91,"line":701},[216,1363,1364],{"class":225},"action ",[216,1366,718],{"class":221},[216,1368,1369],{"class":225}," QAction(icon(",[216,1371,1372],{"class":266},"\"export.svg\"",[216,1374,1375],{"class":225},"), ",[216,1377,1378],{"class":266},"\"Export to asset system\"",[216,1380,727],{"class":225},[216,1382,1383],{"class":91,"line":707},[216,1384,247],{"emptyLinePlaceholder":246},[216,1386,1387,1390,1392,1395,1397,1399,1402,1405,1408,1411,1414,1417,1419],{"class":91,"line":712},[216,1388,1389],{"class":225},"asset_id_validator ",[216,1391,718],{"class":221},[216,1393,1394],{"class":225}," QRegularExpressionValidator(QRegularExpression(",[216,1396,273],{"class":221},[216,1398,276],{"class":266},[216,1400,1401],{"class":253},"^[A-Z]",[216,1403,1404],{"class":221},"{2}",[216,1406,1407],{"class":282},"-",[216,1409,1410],{"class":253},"\\d",[216,1412,1413],{"class":221},"{5}",[216,1415,1416],{"class":253},"$",[216,1418,276],{"class":266},[216,1420,1421],{"class":225},"))\n",[216,1423,1424],{"class":91,"line":730},[216,1425,247],{"emptyLinePlaceholder":246},[216,1427,1428,1431,1433,1436,1439],{"class":91,"line":757},[216,1429,1430],{"class":225},"new_field ",[216,1432,718],{"class":221},[216,1434,1435],{"class":225}," QgsField(",[216,1437,1438],{"class":266},"\"inspected_on\"",[216,1440,1441],{"class":225},", QMetaType.Type.QDate)\n",[216,1443,1444],{"class":91,"line":777},[216,1445,247],{"emptyLinePlaceholder":246},[216,1447,1448,1451,1453],{"class":91,"line":810},[216,1449,1450],{"class":225},"result ",[216,1452,718],{"class":221},[216,1454,1455],{"class":225}," dialog.exec()\n",[14,1457,1458,1460,1461,1464,1465,1468,1469,1471,1472,1475,1476,1478,1479,1481,1482,1484,1485,1487,1488,1491,1492,1495,1496,1499,1500,1502,1503,1506,1507,1510,1511,1502,1514,1517],{},[176,1459,947],{}," PyQt6 does not include ",[27,1462,1463],{},"pyrcc",", so compiled ",[27,1466,1467],{},"resources.py"," files are the most common hard blocker; loading icons from files next to the plugin by absolute path works identically on both versions and removes a build step. ",[27,1470,585],{}," moved to ",[27,1473,1474],{},"QtGui"," in Qt6, and the import fallback keeps older 3.x builds working. ",[27,1477,137],{}," and its validator no longer exist; ",[27,1480,140],{}," has been available throughout Qt5 and behaves the same for typical patterns, though it does not anchor automatically — add ",[27,1483,571],{}," and ",[27,1486,1416],{},". ",[27,1489,1490],{},"QgsField"," accepts ",[27,1493,1494],{},"QMetaType.Type"," from QGIS 3.38, which is the main reason to set that as the minimum version. ",[27,1497,1498],{},"exec()"," works on PyQt5 5.15 as well as PyQt6. Other removals to look for include ",[27,1501,645],{}," (use ",[27,1504,1505],{},"QScreen"," via ",[27,1508,1509],{},"QGuiApplication.primaryScreen()",") and ",[27,1512,1513],{},"QFontMetrics.width",[27,1515,1516],{},"horizontalAdvance",").",[162,1519,1521],{"id":1520},"declare-support-in-metadatatxt","Declare support in metadata.txt",[14,1523,1524,1525,1527,1528,1531,1532,1535],{},"The plugin repository uses ",[27,1526,29],{}," to decide which QGIS versions see a plugin. By default a plugin's maximum supported version is the ",[27,1529,1530],{},".99"," release of the major version of its minimum — so a plugin declaring ",[27,1533,1534],{},"qgisMinimumVersion=3.28"," is assumed to stop at 3.99 and is hidden from QGIS 4 users.",[14,1537,1538],{},[34,1539,1542,1545,1548,1550,1553,1559,1564,1567,1571,1575,1578,1582,1585,1588,1592],{"viewBox":1540,"role":37,"ariaLabel":1541,"xmlns":39},"0 0 760 236","How metadata.txt version fields affect visibility: a plugin with only qgisMinimumVersion 3.28 is capped at 3.99 and hidden in QGIS 4, while adding qgisMaximumVersion 4.99 and the Qt6 support flag makes it available to both generations",[41,1543,1544],{},"Tell the repository the plugin is ready",[45,1546,1547],{},"Left: metadata with qgisMinimumVersion 3.28 only. The implied maximum is 3.99, so QGIS 3.40 and 3.44 users see the plugin and QGIS 4 users do not. Right: metadata with qgisMinimumVersion 3.38, qgisMaximumVersion 4.99 and supportsQt6 equals True. QGIS 3.38 and later and QGIS 4 users all see it.",[49,1549],{"x":51,"y":51,"width":52,"height":69,"fill":54},[56,1551,1552],{"x":58,"y":1010,"style":60,"fill":61,"textAnchor":62},"A ported plugin nobody can install is not ported",[49,1554],{"x":66,"y":67,"width":1555,"height":1556,"rx":70,"fill":1053,"stroke":1557,"style":1558},"344","172","#b91c1c","stroke-width:2.5",[56,1560,1563],{"x":1561,"y":77,"style":1562,"fill":1557,"textAnchor":62},"196","text-anchor:middle;font-size:11.5px;font-weight:bold;font-family:sans-serif","before",[56,1565,1534],{"x":1561,"y":1566,"style":106,"fill":102,"textAnchor":62},"100",[56,1568,1570],{"x":1561,"y":1569,"style":1116,"fill":102,"textAnchor":62},"138","implied maximum: 3.99",[56,1572,1574],{"x":1561,"y":1573,"style":78,"fill":1557,"textAnchor":62},"178","hidden in QGIS 4",[49,1576],{"x":1577,"y":67,"width":1555,"height":1556,"rx":70,"fill":1099,"stroke":1100,"style":1558},"392",[56,1579,1581],{"x":1580,"y":77,"style":1562,"fill":88,"textAnchor":62},"564","after",[56,1583,1584],{"x":1580,"y":1566,"style":106,"fill":102,"textAnchor":62},"qgisMinimumVersion=3.38",[56,1586,1587],{"x":1580,"y":1109,"style":106,"fill":102,"textAnchor":62},"qgisMaximumVersion=4.99",[56,1589,1591],{"x":1580,"y":1590,"style":106,"fill":102,"textAnchor":62},"140","supportsQt6=True",[56,1593,1594],{"x":1580,"y":1573,"style":78,"fill":88,"textAnchor":62},"offered to 3.38+ and QGIS 4",[207,1596,1600],{"className":1597,"code":1598,"language":1599,"meta":212,"style":212},"language-ini shiki shiki-themes github-dark","[general]\nname=Asset Tools\nqgisMinimumVersion=3.38\nqgisMaximumVersion=4.99\nsupportsQt6=True\nversion=3.0.0\nchangelog=\n    3.0.0 Runs on QGIS 4 (Qt6) and QGIS 3.38 or newer; icons no longer use compiled resources\n","ini",[27,1601,1602,1607,1612,1617,1622,1627,1632,1637],{"__ignoreMap":212},[216,1603,1604],{"class":91,"line":218},[216,1605,1606],{},"[general]\n",[216,1608,1609],{"class":91,"line":229},[216,1610,1611],{},"name=Asset Tools\n",[216,1613,1614],{"class":91,"line":243},[216,1615,1616],{},"qgisMinimumVersion=3.38\n",[216,1618,1619],{"class":91,"line":250},[216,1620,1621],{},"qgisMaximumVersion=4.99\n",[216,1623,1624],{"class":91,"line":263},[216,1625,1626],{},"supportsQt6=True\n",[216,1628,1629],{"class":91,"line":303},[216,1630,1631],{},"version=3.0.0\n",[216,1633,1634],{"class":91,"line":329},[216,1635,1636],{},"changelog=\n",[216,1638,1639],{"class":91,"line":351},[216,1640,1641],{},"    3.0.0 Runs on QGIS 4 (Qt6) and QGIS 3.38 or newer; icons no longer use compiled resources\n",[14,1643,1644,1646,1647,1649,1650,1652,1653,1657,1658,1662],{},[176,1645,947],{}," ",[27,1648,1587],{}," extends the range explicitly into the 4 series. ",[27,1651,1591],{}," is the flag the plugin repository and plugin manager use to mark a plugin as tested with Qt6 builds, which also covers Qt6 builds of late 3.x. Bumping the major version of the plugin signals the raised minimum QGIS version to users who pin versions. Writing the rest of the file is covered in ",[21,1654,1656],{"href":1655},"\u002Fqgis-plugin-development\u002Fpublishing-to-the-qgis-plugin-repository\u002Fwrite-metadata-txt-qgis-plugin\u002F","writing metadata.txt for a QGIS plugin",", and the release mechanics in ",[21,1659,1661],{"href":1660},"\u002Fqgis-plugin-development\u002Fpublishing-to-the-qgis-plugin-repository\u002Fversion-and-changelog-qgis-plugin\u002F","versioning and changelog",".",[162,1664,1666],{"id":1665},"test-on-both-generations","Test on both generations",[14,1668,1669],{},"A port is only done when the tests pass on the oldest supported 3.x release and on QGIS 4. A CI matrix with one job per QGIS image does that on every push.",[207,1671,1675],{"className":1672,"code":1673,"language":1674,"meta":212,"style":212},"language-yaml shiki shiki-themes github-dark","jobs:\n  test:\n    runs-on: ubuntu-latest\n    strategy:\n      fail-fast: false\n      matrix:\n        qgis_image: [\"qgis\u002Fqgis:3.40\", \"qgis\u002Fqgis:3.44\", \"qgis\u002Fqgis:4.0\"]\n    container: ${{ matrix.qgis_image }}\n    steps:\n      - uses: actions\u002Fcheckout@v4\n      - run: pip3 install --break-system-packages pytest pytest-qgis\n      - run: xvfb-run -a python3 -m pytest -q tests\u002F\n","yaml",[27,1676,1677,1685,1692,1702,1709,1719,1726,1750,1760,1767,1780,1792],{"__ignoreMap":212},[216,1678,1679,1683],{"class":91,"line":218},[216,1680,1682],{"class":1681},"s4JwU","jobs",[216,1684,1258],{"class":225},[216,1686,1687,1690],{"class":91,"line":229},[216,1688,1689],{"class":1681},"  test",[216,1691,1258],{"class":225},[216,1693,1694,1697,1699],{"class":91,"line":243},[216,1695,1696],{"class":1681},"    runs-on",[216,1698,270],{"class":225},[216,1700,1701],{"class":266},"ubuntu-latest\n",[216,1703,1704,1707],{"class":91,"line":250},[216,1705,1706],{"class":1681},"    strategy",[216,1708,1258],{"class":225},[216,1710,1711,1714,1716],{"class":91,"line":263},[216,1712,1713],{"class":1681},"      fail-fast",[216,1715,270],{"class":225},[216,1717,1718],{"class":253},"false\n",[216,1720,1721,1724],{"class":91,"line":303},[216,1722,1723],{"class":1681},"      matrix",[216,1725,1258],{"class":225},[216,1727,1728,1731,1734,1737,1739,1742,1744,1747],{"class":91,"line":329},[216,1729,1730],{"class":1681},"        qgis_image",[216,1732,1733],{"class":225},": [",[216,1735,1736],{"class":266},"\"qgis\u002Fqgis:3.40\"",[216,1738,797],{"class":225},[216,1740,1741],{"class":266},"\"qgis\u002Fqgis:3.44\"",[216,1743,797],{"class":225},[216,1745,1746],{"class":266},"\"qgis\u002Fqgis:4.0\"",[216,1748,1749],{"class":225},"]\n",[216,1751,1752,1755,1757],{"class":91,"line":351},[216,1753,1754],{"class":1681},"    container",[216,1756,270],{"class":225},[216,1758,1759],{"class":266},"${{ matrix.qgis_image }}\n",[216,1761,1762,1765],{"class":91,"line":411},[216,1763,1764],{"class":1681},"    steps",[216,1766,1258],{"class":225},[216,1768,1769,1772,1775,1777],{"class":91,"line":485},[216,1770,1771],{"class":225},"      - ",[216,1773,1774],{"class":1681},"uses",[216,1776,270],{"class":225},[216,1778,1779],{"class":266},"actions\u002Fcheckout@v4\n",[216,1781,1782,1784,1787,1789],{"class":91,"line":553},[216,1783,1771],{"class":225},[216,1785,1786],{"class":1681},"run",[216,1788,270],{"class":225},[216,1790,1791],{"class":266},"pip3 install --break-system-packages pytest pytest-qgis\n",[216,1793,1794,1796,1798,1800],{"class":91,"line":594},[216,1795,1771],{"class":225},[216,1797,1786],{"class":1681},[216,1799,270],{"class":225},[216,1801,1802],{"class":266},"xvfb-run -a python3 -m pytest -q tests\u002F\n",[14,1804,1805,1646,1807,1810,1811,1814,1815,1818,1819,1823,1824,1827],{},[176,1806,947],{},[27,1808,1809],{},"fail-fast: false"," lets every job finish, so a failure on QGIS 4 does not hide whether 3.40 still passes. Official ",[27,1812,1813],{},"qgis\u002Fqgis"," container images are published per release; check the tags available for the 4 series when you set this up. Running under ",[27,1816,1817],{},"xvfb-run"," gives Qt a virtual display for tests that create widgets. The workflow builds on ",[21,1820,1822],{"href":1821},"\u002Fqgis-plugin-development\u002Ftesting-and-ci-for-plugins\u002Frun-qgis-plugin-tests-in-github-actions\u002F","running QGIS plugin tests in GitHub Actions",", and dialog tests are where most Qt6 regressions show up, because enums and ",[27,1825,1826],{},"exec"," calls live in UI code.",[162,1829,1831],{"id":1830},"qgis-version-compatibility","QGIS version compatibility",[14,1833,1834,1835,1837],{},"The scoped enum forms have been accepted by PyQt5 for most Qt enums for years and by the QGIS API since the 3.2x releases; ",[27,1836,186],{}," field types need 3.38. The migration script lives in the QGIS source repository and improves over time, so fetch a current copy rather than an old one. QGIS 4 removes APIs deprecated during 3.x — the compiler warnings and deprecation notices in 3.40 and 3.44 are the best list of what else to check in your own code.",[162,1839,1841],{"id":1840},"troubleshooting","Troubleshooting",[167,1843,1844,1853,1861,1870,1884],{},[170,1845,1846,1646,1849,1852],{},[176,1847,1848],{},"The plugin does not appear in QGIS 4's plugin manager.",[27,1850,1851],{},"qgisMaximumVersion"," is missing or below 4.0.",[170,1854,1855,1860],{},[176,1856,1857,1662],{},[27,1858,1859],{},"AttributeError: type object 'Qt' has no attribute 'AlignLeft'"," An unscoped enum survived the port.",[170,1862,1863,1866,1867,1869],{},[176,1864,1865],{},"Icons are blank on QGIS 4."," They came from a compiled ",[27,1868,1467],{},"; load them from files.",[170,1871,1872,1877,1878,1880,1881,1662],{},[176,1873,1874,1662],{},[27,1875,1876],{},"ImportError: cannot import name 'QAction'"," Import from ",[27,1879,1474],{}," with a fallback to ",[27,1882,1883],{},"QtWidgets",[170,1885,1886,1889,1890,1662],{},[176,1887,1888],{},"Works on 3.44, fails on 3.34."," The new spellings need a newer 3.x; raise ",[27,1891,1892],{},"qgisMinimumVersion",[162,1894,1896],{"id":1895},"conclusion","Conclusion",[14,1898,1899,1900,797,1902,1904,1905,797,1907,1909,1910,1912,1913,1484,1915,1917],{},"Audit the plugin, run the QGIS migration script on a branch, review its diff, and make the manual fixes: icons from files, ",[27,1901,140],{},[27,1903,585],{}," from ",[27,1906,1474],{},[27,1908,186],{}," field types and ",[27,1911,1498],{},". Set the minimum version to 3.38, declare ",[27,1914,1587],{},[27,1916,1591],{},", and prove it with a CI matrix that runs the tests on a 3.x LTR and on QGIS 4 from the same code.",[162,1919,1921],{"id":1920},"frequently-asked-questions","Frequently Asked Questions",[14,1923,1924,1927],{},[176,1925,1926],{},"Do I need separate plugin versions for QGIS 3 and QGIS 4?","\nUsually not. With scoped enums and the fixes above, one codebase runs on both.",[14,1929,1930,1936,1937,1940],{},[176,1931,1932,1933,1935],{},"Will ",[27,1934,957],{}," files from Qt Designer still work?","\nYes. ",[27,1938,1939],{},"uic.loadUiType"," exists in both, though custom widgets referenced in the file must themselves be Qt6-compatible.",[14,1942,1943,1946,1947,1662],{},[176,1944,1945],{},"What about plugins that bundle compiled Python extensions?","\nThey must be rebuilt for the Python version QGIS 4 ships; see ",[21,1948,1950],{"href":1949},"\u002Fqgis-plugin-development\u002Fplugin-boilerplate-structure\u002Fbundle-third-party-dependencies-in-qgis-plugin\u002F","bundling third-party dependencies",[14,1952,1953,1956],{},[176,1954,1955],{},"Can I test Qt6 before installing QGIS 4?","\nQt6 builds of late 3.x releases exist on some platforms and package managers; they catch most Qt-side problems early.",[162,1958,1960],{"id":1959},"related","Related",[167,1962,1963,1968,1974,1979,1985],{},[170,1964,1965,1967],{},[21,1966,24],{"href":23}," — the guide this recipe belongs to",[170,1969,1970],{},[21,1971,1973],{"href":1972},"\u002Fpyqgis-fundamentals-environment-setup\u002Fqgis-api-architecture\u002Fqgis-python-version-compatibility-guide\u002F","QGIS Python Version Compatibility Guide",[170,1975,1976],{},[21,1977,1978],{"href":1655},"Write metadata.txt for a QGIS Plugin",[170,1980,1981],{},[21,1982,1984],{"href":1983},"\u002Fqgis-plugin-development\u002Fplugin-boilerplate-structure\u002Fadd-plugin-icon-and-resources-pyqgis\u002F","Add a Plugin Icon and Resources File",[170,1986,1987],{},[21,1988,1989],{"href":1821},"Run QGIS Plugin Tests in GitHub Actions",[1991,1992,1993],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sns5M, html code.shiki .sns5M{--shiki-default:#DBEDFF}html pre.shiki code .sRjNt, html code.shiki .sRjNt{--shiki-default:#85E89D;--shiki-default-font-weight:bold}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s4JwU, html code.shiki .s4JwU{--shiki-default:#85E89D}",{"title":212,"searchDepth":229,"depth":229,"links":1995},[1996,1997,1998,1999,2000,2001,2002,2003,2004,2005,2006],{"id":164,"depth":229,"text":165},{"id":201,"depth":229,"text":202},{"id":965,"depth":229,"text":966},{"id":1210,"depth":229,"text":1211},{"id":1520,"depth":229,"text":1521},{"id":1665,"depth":229,"text":1666},{"id":1830,"depth":229,"text":1831},{"id":1840,"depth":229,"text":1841},{"id":1895,"depth":229,"text":1896},{"id":1920,"depth":229,"text":1921},{"id":1959,"depth":229,"text":1960},"Make a PyQGIS plugin run on the Qt6-based QGIS 4 series while still supporting the 3.x LTRs — auditing for Qt5-only code, scoped enums, exec(), moved classes, resources, QVariant field types, the migration script, metadata.txt version flags and a test matrix.","md",{"slug":2010,"type":2011,"breadcrumb":2012,"datePublished":2013,"dateModified":2013},"port-qgis-plugin-to-qgis-4-and-qt6","article","Port a Plugin to QGIS 4 and Qt6","2026-09-17","\u002Fqgis-plugin-development\u002Fplugin-boilerplate-structure\u002Fport-qgis-plugin-to-qgis-4-and-qt6",{"title":5,"description":2007},"qgis-plugin-development\u002Fplugin-boilerplate-structure\u002Fport-qgis-plugin-to-qgis-4-and-qt6\u002Findex","3dXHDO1vnyIOR3opNRNKLP-ryt3qzoozDaN01F_wi0c",1789632907527]