[{"data":1,"prerenderedAt":2097},["ShallowReactive",2],{"doc:\u002Fqgis-plugin-development\u002Fplugin-settings-and-localization\u002Fstore-per-project-plugin-settings-pyqgis":3},{"id":4,"title":5,"body":6,"description":2086,"extension":2087,"meta":2088,"navigation":201,"path":2093,"seo":2094,"stem":2095,"__hash__":2096},"docs\u002Fqgis-plugin-development\u002Fplugin-settings-and-localization\u002Fstore-per-project-plugin-settings-pyqgis\u002Findex.md","Store Per-Project Plugin Settings in PyQGIS",{"type":7,"value":8,"toc":2071},"minimark",[9,13,22,31,141,146,161,165,168,450,469,473,476,536,698,716,720,723,1045,1058,1062,1065,1121,1441,1458,1462,1465,1542,1559,1563,1569,1752,1761,1765,1772,1903,1920,1924,1942,1946,1978,1982,1997,2001,2014,2020,2028,2034,2038,2067],[10,11,5],"h1",{"id":12},"store-per-project-plugin-settings-in-pyqgis",[14,15,16,17,21],"p",{},"Some plugin settings belong to the user: the last folder, preferred units, whether to show a welcome message. Others belong to the project: which layer holds the assets, which field is the identifier, which API endpoint this project syncs with, the thresholds agreed for this study area. Storing project-specific settings in the user profile breaks as soon as the user opens a second project — or a colleague opens the first. QGIS projects have their own key–value store for plugins, saved inside the ",[18,19,20],"code",{},".qgz",", so settings travel with the project and every user who opens it sees the same configuration.",[14,23,24,25,30],{},"This recipe belongs to ",[26,27,29],"a",{"href":28},"\u002Fqgis-plugin-development\u002Fplugin-settings-and-localization\u002F","Plugin Settings & Localization",". It writes and reads project entries with types, stores layer references robustly, reacts to projects being opened and saved, and helps decide which storage — project, user or layer — each setting needs.",[14,32,33],{},[34,35,40,44,48,55,64,74,80,86,90,94,99,104,107,110,113,116,119,124,129,133,137],"svg",{"viewBox":36,"role":37,"ariaLabel":38,"xmlns":39},"0 0 760 280","img","Three stores for plugin settings — user settings, project entries and layer properties — and what each is for","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[41,42,43],"title",{},"Where a setting belongs",[45,46,47],"desc",{},"User settings via QgsSettings live in the profile and follow the person across projects: last folder, units, UI preferences. Project entries via writeEntry live in the project file and follow the project across people: which layer, which endpoint, agreed thresholds. Layer custom properties live with one layer and travel in its style: per-layer field mappings. Choosing the right store is the main design decision.",[49,50],"rect",{"x":51,"y":51,"width":52,"height":53,"fill":54},"0","760","280","#f6f3ea",[56,57,63],"text",{"x":58,"y":59,"style":60,"fill":61,"textAnchor":62},"380","28","text-anchor:middle;font-size:14px;font-family:sans-serif;font-weight:bold","#17211d","middle","Follows the person, the project, or the layer?",[49,65],{"x":66,"y":67,"width":68,"height":69,"rx":70,"fill":71,"stroke":72,"style":73},"24","56","222","196","8","#eff3ff","#2563eb","stroke-width:2",[56,75,79],{"x":76,"y":77,"style":78,"fill":72,"textAnchor":62},"135","97.6","text-anchor:middle;font-size:11.0px;font-family:sans-serif;font-weight:bold","QgsSettings",[56,81,85],{"x":76,"y":82,"style":83,"fill":84,"textAnchor":62},"127.6","text-anchor:middle;font-size:10.0px;font-family:sans-serif","#2f3b35","user profile",[56,87,89],{"x":76,"y":88,"style":83,"fill":84,"textAnchor":62},"157.6","last folder, units",[56,91,93],{"x":76,"y":92,"style":83,"fill":84,"textAnchor":62},"187.6","UI preferences",[56,95,98],{"x":76,"y":96,"style":83,"fill":97,"textAnchor":62},"217.6","#59645f","follows the person",[49,100],{"x":101,"y":67,"width":68,"height":69,"rx":70,"fill":102,"stroke":103,"style":73},"269","#eef7f4","#0f766e",[56,105,106],{"x":58,"y":77,"style":78,"fill":103,"textAnchor":62},"project entries",[56,108,109],{"x":58,"y":82,"style":83,"fill":84,"textAnchor":62},"inside .qgz",[56,111,112],{"x":58,"y":88,"style":83,"fill":84,"textAnchor":62},"which layer, endpoint",[56,114,115],{"x":58,"y":92,"style":83,"fill":84,"textAnchor":62},"thresholds",[56,117,118],{"x":58,"y":96,"style":83,"fill":97,"textAnchor":62},"follows the project",[49,120],{"x":121,"y":67,"width":68,"height":69,"rx":70,"fill":122,"stroke":123,"style":73},"514","#fdf2e2","#b45309",[56,125,128],{"x":126,"y":127,"style":78,"fill":123,"textAnchor":62},"625","112.6","layer properties",[56,130,132],{"x":126,"y":131,"style":83,"fill":84,"textAnchor":62},"142.6","with one layer",[56,134,136],{"x":126,"y":135,"style":83,"fill":84,"textAnchor":62},"172.6","field mapping",[56,138,140],{"x":126,"y":139,"style":83,"fill":97,"textAnchor":62},"202.6","travels in style",[142,143,145],"h2",{"id":144},"prerequisites","Prerequisites",[147,148,149,153],"ul",{},[150,151,152],"li",{},"QGIS 3.34 LTR or newer, or the QGIS 4 series.",[150,154,155,156,160],{},"A plugin with settings to store; user-level settings are covered in ",[26,157,159],{"href":158},"\u002Fqgis-plugin-development\u002Fplugin-settings-and-localization\u002Fstore-plugin-settings-with-qgssettings\u002F","storing plugin settings with QgsSettings",".",[142,162,164],{"id":163},"write-and-read-project-entries","Write and read project entries",[14,166,167],{},"Project entries are grouped by a scope — your plugin's name — and a key. Typed write and read methods keep values as numbers, booleans and lists rather than strings.",[169,170,175],"pre",{"className":171,"code":172,"language":173,"meta":174,"style":174},"language-python shiki shiki-themes github-dark","from qgis.core import QgsProject\n\nSCOPE = \"AssetSync\"\nproject = QgsProject.instance()\n\nproject.writeEntry(SCOPE, \"endpoint\", \"https:\u002F\u002Fassets.example.org\u002Fapi\u002Fv2\")\nproject.writeEntryDouble(SCOPE, \"tolerance_m\", 0.5)\nproject.writeEntryBool(SCOPE, \"auto_sync\", True)\nproject.writeEntry(SCOPE, \"sync_fields\", [\"asset_id\", \"status\", \"inspected_on\"])\n\nendpoint, ok = project.readEntry(SCOPE, \"endpoint\", \"\")\ntolerance, _ = project.readDoubleEntry(SCOPE, \"tolerance_m\", 1.0)\nauto_sync, _ = project.readBoolEntry(SCOPE, \"auto_sync\", False)\nfields, _ = project.readListEntry(SCOPE, \"sync_fields\", [])\nprint(endpoint, tolerance, auto_sync, fields, \"found\" if ok else \"default used\")\n","python","",[18,176,177,196,203,217,229,234,256,276,296,327,332,356,380,404,424],{"__ignoreMap":174},[178,179,182,186,190,193],"span",{"class":180,"line":181},"line",1,[178,183,185],{"class":184},"snl16","from",[178,187,189],{"class":188},"s95oV"," qgis.core ",[178,191,192],{"class":184},"import",[178,194,195],{"class":188}," QgsProject\n",[178,197,199],{"class":180,"line":198},2,[178,200,202],{"emptyLinePlaceholder":201},true,"\n",[178,204,206,210,213],{"class":180,"line":205},3,[178,207,209],{"class":208},"sDLfK","SCOPE",[178,211,212],{"class":184}," =",[178,214,216],{"class":215},"sU2Wk"," \"AssetSync\"\n",[178,218,220,223,226],{"class":180,"line":219},4,[178,221,222],{"class":188},"project ",[178,224,225],{"class":184},"=",[178,227,228],{"class":188}," QgsProject.instance()\n",[178,230,232],{"class":180,"line":231},5,[178,233,202],{"emptyLinePlaceholder":201},[178,235,237,240,242,245,248,250,253],{"class":180,"line":236},6,[178,238,239],{"class":188},"project.writeEntry(",[178,241,209],{"class":208},[178,243,244],{"class":188},", ",[178,246,247],{"class":215},"\"endpoint\"",[178,249,244],{"class":188},[178,251,252],{"class":215},"\"https:\u002F\u002Fassets.example.org\u002Fapi\u002Fv2\"",[178,254,255],{"class":188},")\n",[178,257,259,262,264,266,269,271,274],{"class":180,"line":258},7,[178,260,261],{"class":188},"project.writeEntryDouble(",[178,263,209],{"class":208},[178,265,244],{"class":188},[178,267,268],{"class":215},"\"tolerance_m\"",[178,270,244],{"class":188},[178,272,273],{"class":208},"0.5",[178,275,255],{"class":188},[178,277,279,282,284,286,289,291,294],{"class":180,"line":278},8,[178,280,281],{"class":188},"project.writeEntryBool(",[178,283,209],{"class":208},[178,285,244],{"class":188},[178,287,288],{"class":215},"\"auto_sync\"",[178,290,244],{"class":188},[178,292,293],{"class":208},"True",[178,295,255],{"class":188},[178,297,299,301,303,305,308,311,314,316,319,321,324],{"class":180,"line":298},9,[178,300,239],{"class":188},[178,302,209],{"class":208},[178,304,244],{"class":188},[178,306,307],{"class":215},"\"sync_fields\"",[178,309,310],{"class":188},", [",[178,312,313],{"class":215},"\"asset_id\"",[178,315,244],{"class":188},[178,317,318],{"class":215},"\"status\"",[178,320,244],{"class":188},[178,322,323],{"class":215},"\"inspected_on\"",[178,325,326],{"class":188},"])\n",[178,328,330],{"class":180,"line":329},10,[178,331,202],{"emptyLinePlaceholder":201},[178,333,335,338,340,343,345,347,349,351,354],{"class":180,"line":334},11,[178,336,337],{"class":188},"endpoint, ok ",[178,339,225],{"class":184},[178,341,342],{"class":188}," project.readEntry(",[178,344,209],{"class":208},[178,346,244],{"class":188},[178,348,247],{"class":215},[178,350,244],{"class":188},[178,352,353],{"class":215},"\"\"",[178,355,255],{"class":188},[178,357,359,362,364,367,369,371,373,375,378],{"class":180,"line":358},12,[178,360,361],{"class":188},"tolerance, _ ",[178,363,225],{"class":184},[178,365,366],{"class":188}," project.readDoubleEntry(",[178,368,209],{"class":208},[178,370,244],{"class":188},[178,372,268],{"class":215},[178,374,244],{"class":188},[178,376,377],{"class":208},"1.0",[178,379,255],{"class":188},[178,381,383,386,388,391,393,395,397,399,402],{"class":180,"line":382},13,[178,384,385],{"class":188},"auto_sync, _ ",[178,387,225],{"class":184},[178,389,390],{"class":188}," project.readBoolEntry(",[178,392,209],{"class":208},[178,394,244],{"class":188},[178,396,288],{"class":215},[178,398,244],{"class":188},[178,400,401],{"class":208},"False",[178,403,255],{"class":188},[178,405,407,410,412,415,417,419,421],{"class":180,"line":406},14,[178,408,409],{"class":188},"fields, _ ",[178,411,225],{"class":184},[178,413,414],{"class":188}," project.readListEntry(",[178,416,209],{"class":208},[178,418,244],{"class":188},[178,420,307],{"class":215},[178,422,423],{"class":188},", [])\n",[178,425,427,430,433,436,439,442,445,448],{"class":180,"line":426},15,[178,428,429],{"class":208},"print",[178,431,432],{"class":188},"(endpoint, tolerance, auto_sync, fields, ",[178,434,435],{"class":215},"\"found\"",[178,437,438],{"class":184}," if",[178,440,441],{"class":188}," ok ",[178,443,444],{"class":184},"else",[178,446,447],{"class":215}," \"default used\"",[178,449,255],{"class":188},[14,451,452,456,457,460,461,464,465,468],{},[453,454,455],"strong",{},"Breakdown:"," The scope keeps your keys separate from other plugins' — use your plugin's name. Each read returns the value and a flag saying whether it was found, so code can tell a stored value from the default. Typed readers return the right Python type: ",[18,458,459],{},"readDoubleEntry"," a float, ",[18,462,463],{},"readBoolEntry"," a bool, ",[18,466,467],{},"readListEntry"," a list of strings. Writing marks the project as modified; the values are saved into the project file the next time it is saved, by the user or by your code.",[142,470,472],{"id":471},"store-layer-references-that-survive","Store layer references that survive",[14,474,475],{},"The most common project setting is \"which layer\". Storing the layer's name breaks when it is renamed; storing its id survives renaming, reordering and grouping.",[14,477,478],{},[34,479,482,485,488,490,493,498,504,509,513,518,524,528,532],{"viewBox":480,"role":37,"ariaLabel":481,"xmlns":39},"0 0 760 222","Layer settings stored by name breaking after a rename, compared with layer ids that survive and are checked on read",[41,483,484],{},"Store ids, not names",[45,486,487],{},"A setting that stores the layer name assets fails when a user renames the layer to Assets 2026. Storing the layer id, a stable identifier assigned when the layer was added, keeps working after renames. On read, the id is looked up with mapLayer; if the layer was removed, the plugin asks the user to choose again.",[49,489],{"x":51,"y":51,"width":52,"height":68,"fill":54},[56,491,492],{"x":58,"y":59,"style":60,"fill":61,"textAnchor":62},"Names change; ids do not",[49,494],{"x":66,"y":67,"width":495,"height":496,"rx":70,"fill":122,"stroke":497,"style":73},"340","150","#b91c1c",[56,499,503],{"x":500,"y":501,"style":502,"fill":497,"textAnchor":62},"194","106.78","text-anchor:middle;font-size:11.5px;font-family:sans-serif;font-weight:bold","by name",[56,505,508],{"x":500,"y":506,"style":507,"fill":84,"textAnchor":62},"134.78","text-anchor:middle;font-size:10.5px;font-family:sans-serif","\"assets\"",[56,510,512],{"x":500,"y":511,"style":507,"fill":97,"textAnchor":62},"162.78","renamed → lost",[49,514],{"x":515,"y":67,"width":495,"height":496,"rx":70,"fill":516,"stroke":517,"style":73},"396","#e8efe6","#15803d",[56,519,523],{"x":520,"y":521,"style":502,"fill":522,"textAnchor":62},"566","92.78","#166534","by id",[56,525,527],{"x":520,"y":526,"style":507,"fill":84,"textAnchor":62},"120.78","\"assets_8f3c…\"",[56,529,531],{"x":520,"y":530,"style":507,"fill":97,"textAnchor":62},"148.78","renamed → still found",[56,533,535],{"x":520,"y":534,"style":507,"fill":97,"textAnchor":62},"176.78","removed → ask again",[169,537,539],{"className":171,"code":538,"language":173,"meta":174,"style":174},"def set_asset_layer(layer):\n    project.writeEntry(SCOPE, \"asset_layer_id\", layer.id())\n\ndef asset_layer():\n    layer_id, ok = project.readEntry(SCOPE, \"asset_layer_id\", \"\")\n    layer = project.mapLayer(layer_id) if ok and layer_id else None\n    if layer is None:\n        return None          # not configured, or the layer was removed\n    return layer\n\nlyr = asset_layer()\nprint(lyr.name() if lyr else \"asset layer not configured\")\n",[18,540,541,553,568,572,582,603,629,646,657,665,669,679],{"__ignoreMap":174},[178,542,543,546,550],{"class":180,"line":181},[178,544,545],{"class":184},"def",[178,547,549],{"class":548},"svObZ"," set_asset_layer",[178,551,552],{"class":188},"(layer):\n",[178,554,555,558,560,562,565],{"class":180,"line":198},[178,556,557],{"class":188},"    project.writeEntry(",[178,559,209],{"class":208},[178,561,244],{"class":188},[178,563,564],{"class":215},"\"asset_layer_id\"",[178,566,567],{"class":188},", layer.id())\n",[178,569,570],{"class":180,"line":205},[178,571,202],{"emptyLinePlaceholder":201},[178,573,574,576,579],{"class":180,"line":219},[178,575,545],{"class":184},[178,577,578],{"class":548}," asset_layer",[178,580,581],{"class":188},"():\n",[178,583,584,587,589,591,593,595,597,599,601],{"class":180,"line":231},[178,585,586],{"class":188},"    layer_id, ok ",[178,588,225],{"class":184},[178,590,342],{"class":188},[178,592,209],{"class":208},[178,594,244],{"class":188},[178,596,564],{"class":215},[178,598,244],{"class":188},[178,600,353],{"class":215},[178,602,255],{"class":188},[178,604,605,608,610,613,616,618,621,624,626],{"class":180,"line":236},[178,606,607],{"class":188},"    layer ",[178,609,225],{"class":184},[178,611,612],{"class":188}," project.mapLayer(layer_id) ",[178,614,615],{"class":184},"if",[178,617,441],{"class":188},[178,619,620],{"class":184},"and",[178,622,623],{"class":188}," layer_id ",[178,625,444],{"class":184},[178,627,628],{"class":208}," None\n",[178,630,631,634,637,640,643],{"class":180,"line":258},[178,632,633],{"class":184},"    if",[178,635,636],{"class":188}," layer ",[178,638,639],{"class":184},"is",[178,641,642],{"class":208}," None",[178,644,645],{"class":188},":\n",[178,647,648,651,653],{"class":180,"line":278},[178,649,650],{"class":184},"        return",[178,652,642],{"class":208},[178,654,656],{"class":655},"sjoCn","          # not configured, or the layer was removed\n",[178,658,659,662],{"class":180,"line":298},[178,660,661],{"class":184},"    return",[178,663,664],{"class":188}," layer\n",[178,666,667],{"class":180,"line":329},[178,668,202],{"emptyLinePlaceholder":201},[178,670,671,674,676],{"class":180,"line":334},[178,672,673],{"class":188},"lyr ",[178,675,225],{"class":184},[178,677,678],{"class":188}," asset_layer()\n",[178,680,681,683,686,688,691,693,696],{"class":180,"line":358},[178,682,429],{"class":208},[178,684,685],{"class":188},"(lyr.name() ",[178,687,615],{"class":184},[178,689,690],{"class":188}," lyr ",[178,692,444],{"class":184},[178,694,695],{"class":215}," \"asset layer not configured\"",[178,697,255],{"class":188},[14,699,700,702,703,706,707,710,711,715],{},[453,701,455],{}," ",[18,704,705],{},"layer.id()"," is generated when a layer is added and stored in the project, so it stays the same across sessions and renames. Looking it up with ",[18,708,709],{},"mapLayer"," returns None if the layer has since been removed, which the plugin should treat as \"not configured\" and prompt the user — for example by opening its settings dialog with a ",[26,712,714],{"href":713},"\u002Fqgis-plugin-development\u002Fqt-designer-for-gis-interfaces\u002Fuse-qgsmaplayercombobox-in-plugin-dialog-pyqgis\u002F","layer combo box",". Field names are fine to store by name, since renaming fields is rarer and their names are what users see.",[142,717,719],{"id":718},"group-settings-in-a-small-class","Group settings in a small class",[14,721,722],{},"Reading and writing scattered entries throughout a plugin makes settings hard to change. A small class with properties gives one place for keys, types and defaults.",[169,724,726],{"className":171,"code":725,"language":173,"meta":174,"style":174},"class ProjectSettings:\n    def __init__(self, project=None, scope=SCOPE):\n        self.p = project or QgsProject.instance()\n        self.scope = scope\n\n    @property\n    def endpoint(self):\n        return self.p.readEntry(self.scope, \"endpoint\", \"\")[0]\n\n    @endpoint.setter\n    def endpoint(self, value):\n        self.p.writeEntry(self.scope, \"endpoint\", value)\n\n    @property\n    def tolerance_m(self):\n        return self.p.readDoubleEntry(self.scope, \"tolerance_m\", 0.5)[0]\n\n    @tolerance_m.setter\n    def tolerance_m(self, value):\n        self.p.writeEntryDouble(self.scope, \"tolerance_m\", float(value))\n\n    def is_configured(self):\n        return bool(self.endpoint) and asset_layer() is not None\n\ncfg = ProjectSettings()\ncfg.tolerance_m = 0.75\nprint(cfg.endpoint, cfg.tolerance_m, cfg.is_configured())\n",[18,727,728,738,764,782,794,798,806,816,846,850,855,864,880,884,890,899,925,930,936,945,967,972,982,1010,1015,1026,1037],{"__ignoreMap":174},[178,729,730,733,736],{"class":180,"line":181},[178,731,732],{"class":184},"class",[178,734,735],{"class":548}," ProjectSettings",[178,737,645],{"class":188},[178,739,740,743,746,749,751,754,757,759,761],{"class":180,"line":198},[178,741,742],{"class":184},"    def",[178,744,745],{"class":208}," __init__",[178,747,748],{"class":188},"(self, project",[178,750,225],{"class":184},[178,752,753],{"class":208},"None",[178,755,756],{"class":188},", scope",[178,758,225],{"class":184},[178,760,209],{"class":208},[178,762,763],{"class":188},"):\n",[178,765,766,769,772,774,777,780],{"class":180,"line":205},[178,767,768],{"class":208},"        self",[178,770,771],{"class":188},".p ",[178,773,225],{"class":184},[178,775,776],{"class":188}," project ",[178,778,779],{"class":184},"or",[178,781,228],{"class":188},[178,783,784,786,789,791],{"class":180,"line":219},[178,785,768],{"class":208},[178,787,788],{"class":188},".scope ",[178,790,225],{"class":184},[178,792,793],{"class":188}," scope\n",[178,795,796],{"class":180,"line":231},[178,797,202],{"emptyLinePlaceholder":201},[178,799,800,803],{"class":180,"line":236},[178,801,802],{"class":548},"    @",[178,804,805],{"class":208},"property\n",[178,807,808,810,813],{"class":180,"line":258},[178,809,742],{"class":184},[178,811,812],{"class":548}," endpoint",[178,814,815],{"class":188},"(self):\n",[178,817,818,820,823,826,829,832,834,836,838,841,843],{"class":180,"line":278},[178,819,650],{"class":184},[178,821,822],{"class":208}," self",[178,824,825],{"class":188},".p.readEntry(",[178,827,828],{"class":208},"self",[178,830,831],{"class":188},".scope, ",[178,833,247],{"class":215},[178,835,244],{"class":188},[178,837,353],{"class":215},[178,839,840],{"class":188},")[",[178,842,51],{"class":208},[178,844,845],{"class":188},"]\n",[178,847,848],{"class":180,"line":298},[178,849,202],{"emptyLinePlaceholder":201},[178,851,852],{"class":180,"line":329},[178,853,854],{"class":548},"    @endpoint.setter\n",[178,856,857,859,861],{"class":180,"line":334},[178,858,742],{"class":184},[178,860,812],{"class":548},[178,862,863],{"class":188},"(self, value):\n",[178,865,866,868,871,873,875,877],{"class":180,"line":358},[178,867,768],{"class":208},[178,869,870],{"class":188},".p.writeEntry(",[178,872,828],{"class":208},[178,874,831],{"class":188},[178,876,247],{"class":215},[178,878,879],{"class":188},", value)\n",[178,881,882],{"class":180,"line":382},[178,883,202],{"emptyLinePlaceholder":201},[178,885,886,888],{"class":180,"line":406},[178,887,802],{"class":548},[178,889,805],{"class":208},[178,891,892,894,897],{"class":180,"line":426},[178,893,742],{"class":184},[178,895,896],{"class":548}," tolerance_m",[178,898,815],{"class":188},[178,900,902,904,906,909,911,913,915,917,919,921,923],{"class":180,"line":901},16,[178,903,650],{"class":184},[178,905,822],{"class":208},[178,907,908],{"class":188},".p.readDoubleEntry(",[178,910,828],{"class":208},[178,912,831],{"class":188},[178,914,268],{"class":215},[178,916,244],{"class":188},[178,918,273],{"class":208},[178,920,840],{"class":188},[178,922,51],{"class":208},[178,924,845],{"class":188},[178,926,928],{"class":180,"line":927},17,[178,929,202],{"emptyLinePlaceholder":201},[178,931,933],{"class":180,"line":932},18,[178,934,935],{"class":548},"    @tolerance_m.setter\n",[178,937,939,941,943],{"class":180,"line":938},19,[178,940,742],{"class":184},[178,942,896],{"class":548},[178,944,863],{"class":188},[178,946,948,950,953,955,957,959,961,964],{"class":180,"line":947},20,[178,949,768],{"class":208},[178,951,952],{"class":188},".p.writeEntryDouble(",[178,954,828],{"class":208},[178,956,831],{"class":188},[178,958,268],{"class":215},[178,960,244],{"class":188},[178,962,963],{"class":208},"float",[178,965,966],{"class":188},"(value))\n",[178,968,970],{"class":180,"line":969},21,[178,971,202],{"emptyLinePlaceholder":201},[178,973,975,977,980],{"class":180,"line":974},22,[178,976,742],{"class":184},[178,978,979],{"class":548}," is_configured",[178,981,815],{"class":188},[178,983,985,987,990,993,995,998,1000,1003,1005,1008],{"class":180,"line":984},23,[178,986,650],{"class":184},[178,988,989],{"class":208}," bool",[178,991,992],{"class":188},"(",[178,994,828],{"class":208},[178,996,997],{"class":188},".endpoint) ",[178,999,620],{"class":184},[178,1001,1002],{"class":188}," asset_layer() ",[178,1004,639],{"class":184},[178,1006,1007],{"class":184}," not",[178,1009,628],{"class":208},[178,1011,1013],{"class":180,"line":1012},24,[178,1014,202],{"emptyLinePlaceholder":201},[178,1016,1018,1021,1023],{"class":180,"line":1017},25,[178,1019,1020],{"class":188},"cfg ",[178,1022,225],{"class":184},[178,1024,1025],{"class":188}," ProjectSettings()\n",[178,1027,1029,1032,1034],{"class":180,"line":1028},26,[178,1030,1031],{"class":188},"cfg.tolerance_m ",[178,1033,225],{"class":184},[178,1035,1036],{"class":208}," 0.75\n",[178,1038,1040,1042],{"class":180,"line":1039},27,[178,1041,429],{"class":208},[178,1043,1044],{"class":188},"(cfg.endpoint, cfg.tolerance_m, cfg.is_configured())\n",[14,1046,1047,1049,1050,1053,1054,1057],{},[453,1048,455],{}," Properties hide the scope, keys and typed calls, so the rest of the plugin reads ",[18,1051,1052],{},"cfg.tolerance_m"," like an ordinary attribute. Defaults live in one place. ",[18,1055,1056],{},"is_configured"," answers the question every action asks before running. Adding a setting is a matter of adding a property, and renaming a key — for a new plugin version — happens in one spot, where a migration from the old key can also live.",[142,1059,1061],{"id":1060},"react-to-projects-being-opened-and-saved","React to projects being opened and saved",[14,1063,1064],{},"Settings must be re-read when the user opens another project, and the plugin may want to write its state just before the project is saved.",[14,1066,1067],{},[34,1068,1071,1074,1077,1079,1082,1086,1090,1094,1098,1101,1104,1107,1110,1112,1115,1118],{"viewBox":1069,"role":37,"ariaLabel":1070,"xmlns":39},"0 0 760 196","Project signals for reading, clearing and writing that a plugin connects to reload, reset and save its settings",[41,1072,1073],{},"Project lifecycle signals",[45,1075,1076],{},"When a project is read, readProject fires and the plugin reloads its settings and updates its interface. When a project is cleared, cleared fires and the plugin resets to the unconfigured state. Just before saving, writeProject fires and the plugin writes any pending state. Connecting all three keeps the plugin in step with whichever project is open.",[49,1078],{"x":51,"y":51,"width":52,"height":69,"fill":54},[56,1080,1081],{"x":58,"y":59,"style":60,"fill":61,"textAnchor":62},"Read, clear, write",[49,1083],{"x":66,"y":1084,"width":68,"height":1085,"rx":70,"fill":102,"stroke":103,"style":73},"60","120",[56,1087,1089],{"x":76,"y":1088,"style":502,"fill":103,"textAnchor":62},"97.78","readProject",[56,1091,1093],{"x":76,"y":1092,"style":507,"fill":97,"textAnchor":62},"123.78","reload settings",[56,1095,1097],{"x":76,"y":1096,"style":507,"fill":97,"textAnchor":62},"149.78","update UI",[49,1099],{"x":101,"y":1084,"width":68,"height":1085,"rx":70,"fill":1100,"stroke":97,"style":73},"#fffdf7",[56,1102,1103],{"x":58,"y":1088,"style":502,"fill":61,"textAnchor":62},"cleared",[56,1105,1106],{"x":58,"y":1092,"style":507,"fill":97,"textAnchor":62},"reset to",[56,1108,1109],{"x":58,"y":1096,"style":507,"fill":97,"textAnchor":62},"unconfigured",[49,1111],{"x":121,"y":1084,"width":68,"height":1085,"rx":70,"fill":122,"stroke":123,"style":73},[56,1113,1114],{"x":126,"y":1088,"style":502,"fill":123,"textAnchor":62},"writeProject",[56,1116,1117],{"x":126,"y":1092,"style":507,"fill":97,"textAnchor":62},"write pending",[56,1119,1120],{"x":126,"y":1096,"style":507,"fill":97,"textAnchor":62},"state",[169,1122,1124],{"className":171,"code":1123,"language":173,"meta":174,"style":174},"class AssetSyncPlugin:\n    def __init__(self, iface):\n        self.iface = iface\n        self.cfg = ProjectSettings()\n\n    def initGui(self):\n        p = QgsProject.instance()\n        p.readProject.connect(self.on_project_read)\n        p.cleared.connect(self.on_project_cleared)\n        p.writeProject.connect(self.on_project_write)\n        self.on_project_read()\n\n    def on_project_read(self, *args):\n        state = \"configured\" if self.cfg.is_configured() else \"not configured\"\n        self.iface.messageBar().pushInfo(\"Asset Sync\", f\"Project {state}\")\n\n    def on_project_cleared(self):\n        pass                                   # reset dock widgets, caches\n\n    def on_project_write(self, *args):\n        QgsProject.instance().writeEntry(SCOPE, \"last_saved_by_plugin\", \"1.4.0\")\n\n    def unload(self):\n        p = QgsProject.instance()\n        for sig, slot in ((p.readProject, self.on_project_read), (p.cleared, self.on_project_cleared),\n                          (p.writeProject, self.on_project_write)):\n            try:\n                sig.disconnect(slot)\n            except TypeError:\n                pass\n",[18,1125,1126,1135,1144,1156,1167,1171,1180,1189,1199,1209,1219,1226,1230,1246,1268,1299,1303,1312,1320,1324,1337,1356,1360,1369,1377,1401,1411,1418,1424,1435],{"__ignoreMap":174},[178,1127,1128,1130,1133],{"class":180,"line":181},[178,1129,732],{"class":184},[178,1131,1132],{"class":548}," AssetSyncPlugin",[178,1134,645],{"class":188},[178,1136,1137,1139,1141],{"class":180,"line":198},[178,1138,742],{"class":184},[178,1140,745],{"class":208},[178,1142,1143],{"class":188},"(self, iface):\n",[178,1145,1146,1148,1151,1153],{"class":180,"line":205},[178,1147,768],{"class":208},[178,1149,1150],{"class":188},".iface ",[178,1152,225],{"class":184},[178,1154,1155],{"class":188}," iface\n",[178,1157,1158,1160,1163,1165],{"class":180,"line":219},[178,1159,768],{"class":208},[178,1161,1162],{"class":188},".cfg ",[178,1164,225],{"class":184},[178,1166,1025],{"class":188},[178,1168,1169],{"class":180,"line":231},[178,1170,202],{"emptyLinePlaceholder":201},[178,1172,1173,1175,1178],{"class":180,"line":236},[178,1174,742],{"class":184},[178,1176,1177],{"class":548}," initGui",[178,1179,815],{"class":188},[178,1181,1182,1185,1187],{"class":180,"line":258},[178,1183,1184],{"class":188},"        p ",[178,1186,225],{"class":184},[178,1188,228],{"class":188},[178,1190,1191,1194,1196],{"class":180,"line":278},[178,1192,1193],{"class":188},"        p.readProject.connect(",[178,1195,828],{"class":208},[178,1197,1198],{"class":188},".on_project_read)\n",[178,1200,1201,1204,1206],{"class":180,"line":298},[178,1202,1203],{"class":188},"        p.cleared.connect(",[178,1205,828],{"class":208},[178,1207,1208],{"class":188},".on_project_cleared)\n",[178,1210,1211,1214,1216],{"class":180,"line":329},[178,1212,1213],{"class":188},"        p.writeProject.connect(",[178,1215,828],{"class":208},[178,1217,1218],{"class":188},".on_project_write)\n",[178,1220,1221,1223],{"class":180,"line":334},[178,1222,768],{"class":208},[178,1224,1225],{"class":188},".on_project_read()\n",[178,1227,1228],{"class":180,"line":358},[178,1229,202],{"emptyLinePlaceholder":201},[178,1231,1232,1234,1237,1240,1243],{"class":180,"line":382},[178,1233,742],{"class":184},[178,1235,1236],{"class":548}," on_project_read",[178,1238,1239],{"class":188},"(self, ",[178,1241,1242],{"class":184},"*",[178,1244,1245],{"class":188},"args):\n",[178,1247,1248,1251,1253,1256,1258,1260,1263,1265],{"class":180,"line":406},[178,1249,1250],{"class":188},"        state ",[178,1252,225],{"class":184},[178,1254,1255],{"class":215}," \"configured\"",[178,1257,438],{"class":184},[178,1259,822],{"class":208},[178,1261,1262],{"class":188},".cfg.is_configured() ",[178,1264,444],{"class":184},[178,1266,1267],{"class":215}," \"not configured\"\n",[178,1269,1270,1272,1275,1278,1280,1283,1286,1289,1291,1294,1297],{"class":180,"line":426},[178,1271,768],{"class":208},[178,1273,1274],{"class":188},".iface.messageBar().pushInfo(",[178,1276,1277],{"class":215},"\"Asset Sync\"",[178,1279,244],{"class":188},[178,1281,1282],{"class":184},"f",[178,1284,1285],{"class":215},"\"Project ",[178,1287,1288],{"class":208},"{",[178,1290,1120],{"class":188},[178,1292,1293],{"class":208},"}",[178,1295,1296],{"class":215},"\"",[178,1298,255],{"class":188},[178,1300,1301],{"class":180,"line":901},[178,1302,202],{"emptyLinePlaceholder":201},[178,1304,1305,1307,1310],{"class":180,"line":927},[178,1306,742],{"class":184},[178,1308,1309],{"class":548}," on_project_cleared",[178,1311,815],{"class":188},[178,1313,1314,1317],{"class":180,"line":932},[178,1315,1316],{"class":184},"        pass",[178,1318,1319],{"class":655},"                                   # reset dock widgets, caches\n",[178,1321,1322],{"class":180,"line":938},[178,1323,202],{"emptyLinePlaceholder":201},[178,1325,1326,1328,1331,1333,1335],{"class":180,"line":947},[178,1327,742],{"class":184},[178,1329,1330],{"class":548}," on_project_write",[178,1332,1239],{"class":188},[178,1334,1242],{"class":184},[178,1336,1245],{"class":188},[178,1338,1339,1342,1344,1346,1349,1351,1354],{"class":180,"line":969},[178,1340,1341],{"class":188},"        QgsProject.instance().writeEntry(",[178,1343,209],{"class":208},[178,1345,244],{"class":188},[178,1347,1348],{"class":215},"\"last_saved_by_plugin\"",[178,1350,244],{"class":188},[178,1352,1353],{"class":215},"\"1.4.0\"",[178,1355,255],{"class":188},[178,1357,1358],{"class":180,"line":974},[178,1359,202],{"emptyLinePlaceholder":201},[178,1361,1362,1364,1367],{"class":180,"line":984},[178,1363,742],{"class":184},[178,1365,1366],{"class":548}," unload",[178,1368,815],{"class":188},[178,1370,1371,1373,1375],{"class":180,"line":1012},[178,1372,1184],{"class":188},[178,1374,225],{"class":184},[178,1376,228],{"class":188},[178,1378,1379,1382,1385,1388,1391,1393,1396,1398],{"class":180,"line":1017},[178,1380,1381],{"class":184},"        for",[178,1383,1384],{"class":188}," sig, slot ",[178,1386,1387],{"class":184},"in",[178,1389,1390],{"class":188}," ((p.readProject, ",[178,1392,828],{"class":208},[178,1394,1395],{"class":188},".on_project_read), (p.cleared, ",[178,1397,828],{"class":208},[178,1399,1400],{"class":188},".on_project_cleared),\n",[178,1402,1403,1406,1408],{"class":180,"line":1028},[178,1404,1405],{"class":188},"                          (p.writeProject, ",[178,1407,828],{"class":208},[178,1409,1410],{"class":188},".on_project_write)):\n",[178,1412,1413,1416],{"class":180,"line":1039},[178,1414,1415],{"class":184},"            try",[178,1417,645],{"class":188},[178,1419,1421],{"class":180,"line":1420},28,[178,1422,1423],{"class":188},"                sig.disconnect(slot)\n",[178,1425,1427,1430,1433],{"class":180,"line":1426},29,[178,1428,1429],{"class":184},"            except",[178,1431,1432],{"class":208}," TypeError",[178,1434,645],{"class":188},[178,1436,1438],{"class":180,"line":1437},30,[178,1439,1440],{"class":184},"                pass\n",[14,1442,1443,702,1445,1447,1448,1450,1451,1453,1454,1457],{},[453,1444,455],{},[18,1446,1089],{}," fires after a project has been loaded, the right moment to read settings and refresh the plugin's interface. ",[18,1449,1103],{}," fires when the project is closed or a new one started. ",[18,1452,1114],{}," fires as the project is being saved, so entries written in the handler are included in the file — useful for recording the plugin version that last saved the project, which helps migrations. Disconnecting in ",[18,1455,1456],{},"unload"," prevents handlers from firing after the plugin is reloaded.",[142,1459,1461],{"id":1460},"choose-the-right-store","Choose the right store",[14,1463,1464],{},"Each store has a clear purpose; mixing them is the source of most \"my settings disappeared\" reports.",[169,1466,1468],{"className":171,"code":1467,"language":173,"meta":174,"style":174},"from qgis.core import QgsSettings\n\nQgsSettings().setValue(\"AssetSync\u002Flast_export_folder\", \"\u002Fdata\u002Fexports\")   # user\nproject.writeEntry(SCOPE, \"endpoint\", \"https:\u002F\u002Fassets.example.org\u002Fapi\u002Fv2\")  # project\nasset_layer().setCustomProperty(\"assetsync\u002Fid_field\", \"asset_id\")          # layer\n",[18,1469,1470,1481,1485,1504,1524],{"__ignoreMap":174},[178,1471,1472,1474,1476,1478],{"class":180,"line":181},[178,1473,185],{"class":184},[178,1475,189],{"class":188},[178,1477,192],{"class":184},[178,1479,1480],{"class":188}," QgsSettings\n",[178,1482,1483],{"class":180,"line":198},[178,1484,202],{"emptyLinePlaceholder":201},[178,1486,1487,1490,1493,1495,1498,1501],{"class":180,"line":205},[178,1488,1489],{"class":188},"QgsSettings().setValue(",[178,1491,1492],{"class":215},"\"AssetSync\u002Flast_export_folder\"",[178,1494,244],{"class":188},[178,1496,1497],{"class":215},"\"\u002Fdata\u002Fexports\"",[178,1499,1500],{"class":188},")   ",[178,1502,1503],{"class":655},"# user\n",[178,1505,1506,1508,1510,1512,1514,1516,1518,1521],{"class":180,"line":219},[178,1507,239],{"class":188},[178,1509,209],{"class":208},[178,1511,244],{"class":188},[178,1513,247],{"class":215},[178,1515,244],{"class":188},[178,1517,252],{"class":215},[178,1519,1520],{"class":188},")  ",[178,1522,1523],{"class":655},"# project\n",[178,1525,1526,1529,1532,1534,1536,1539],{"class":180,"line":231},[178,1527,1528],{"class":188},"asset_layer().setCustomProperty(",[178,1530,1531],{"class":215},"\"assetsync\u002Fid_field\"",[178,1533,244],{"class":188},[178,1535,313],{"class":215},[178,1537,1538],{"class":188},")          ",[178,1540,1541],{"class":655},"# layer\n",[14,1543,1544,1546,1547,1549,1550,1554,1555,160],{},[453,1545,455],{}," User preferences that should follow a person across projects go in ",[18,1548,79],{},". Configuration that defines how this project works — endpoints, thresholds, layer roles — goes in project entries. Configuration that belongs to one layer wherever it is used — its id field, its sync mapping — goes in layer custom properties, which also travel in the layer's QML style; a ",[26,1551,1553],{"href":1552},"\u002Fqgis-plugin-development\u002Fextending-qgis-with-custom-classes\u002Fadd-custom-layer-properties-page-pyqgis\u002F","custom layer properties page"," is the natural editor for them. Never store secrets in any of these; use the authentication system, as in ",[26,1556,1558],{"href":1557},"\u002Fpyqgis-fundamentals-environment-setup\u002Fheadless-qgis-and-server-automation\u002Fstore-credentials-with-qgsauthmanager-pyqgis\u002F","storing credentials with QgsAuthManager",[142,1560,1562],{"id":1561},"migrate-settings-between-plugin-versions","Migrate settings between plugin versions",[14,1564,1565,1566,1568],{},"Projects outlive plugin versions. A project saved with version 1.2 of your plugin may be opened years later with version 2.0, after keys have been renamed or values restructured. A small migration step on ",[18,1567,1089],{}," keeps old projects working.",[169,1570,1572],{"className":171,"code":1571,"language":173,"meta":174,"style":174},"SCHEMA_VERSION = 2\n\ndef migrate_project_settings(project):\n    version, _ = project.readNumEntry(SCOPE, \"schema_version\", 1)\n    if version \u003C 2:\n        old_layer, found = project.readEntry(SCOPE, \"layer_name\", \"\")\n        if found and old_layer:\n            matches = project.mapLayersByName(old_layer)\n            if matches:\n                project.writeEntry(SCOPE, \"asset_layer_id\", matches[0].id())\n            project.removeEntry(SCOPE, \"layer_name\")\n    if version \u003C SCHEMA_VERSION:\n        project.writeEntry(SCOPE, \"schema_version\", SCHEMA_VERSION)\n",[18,1573,1574,1584,1588,1598,1622,1637,1659,1672,1682,1690,1709,1722,1735],{"__ignoreMap":174},[178,1575,1576,1579,1581],{"class":180,"line":181},[178,1577,1578],{"class":208},"SCHEMA_VERSION",[178,1580,212],{"class":184},[178,1582,1583],{"class":208}," 2\n",[178,1585,1586],{"class":180,"line":198},[178,1587,202],{"emptyLinePlaceholder":201},[178,1589,1590,1592,1595],{"class":180,"line":205},[178,1591,545],{"class":184},[178,1593,1594],{"class":548}," migrate_project_settings",[178,1596,1597],{"class":188},"(project):\n",[178,1599,1600,1603,1605,1608,1610,1612,1615,1617,1620],{"class":180,"line":219},[178,1601,1602],{"class":188},"    version, _ ",[178,1604,225],{"class":184},[178,1606,1607],{"class":188}," project.readNumEntry(",[178,1609,209],{"class":208},[178,1611,244],{"class":188},[178,1613,1614],{"class":215},"\"schema_version\"",[178,1616,244],{"class":188},[178,1618,1619],{"class":208},"1",[178,1621,255],{"class":188},[178,1623,1624,1626,1629,1632,1635],{"class":180,"line":231},[178,1625,633],{"class":184},[178,1627,1628],{"class":188}," version ",[178,1630,1631],{"class":184},"\u003C",[178,1633,1634],{"class":208}," 2",[178,1636,645],{"class":188},[178,1638,1639,1642,1644,1646,1648,1650,1653,1655,1657],{"class":180,"line":236},[178,1640,1641],{"class":188},"        old_layer, found ",[178,1643,225],{"class":184},[178,1645,342],{"class":188},[178,1647,209],{"class":208},[178,1649,244],{"class":188},[178,1651,1652],{"class":215},"\"layer_name\"",[178,1654,244],{"class":188},[178,1656,353],{"class":215},[178,1658,255],{"class":188},[178,1660,1661,1664,1667,1669],{"class":180,"line":258},[178,1662,1663],{"class":184},"        if",[178,1665,1666],{"class":188}," found ",[178,1668,620],{"class":184},[178,1670,1671],{"class":188}," old_layer:\n",[178,1673,1674,1677,1679],{"class":180,"line":278},[178,1675,1676],{"class":188},"            matches ",[178,1678,225],{"class":184},[178,1680,1681],{"class":188}," project.mapLayersByName(old_layer)\n",[178,1683,1684,1687],{"class":180,"line":298},[178,1685,1686],{"class":184},"            if",[178,1688,1689],{"class":188}," matches:\n",[178,1691,1692,1695,1697,1699,1701,1704,1706],{"class":180,"line":329},[178,1693,1694],{"class":188},"                project.writeEntry(",[178,1696,209],{"class":208},[178,1698,244],{"class":188},[178,1700,564],{"class":215},[178,1702,1703],{"class":188},", matches[",[178,1705,51],{"class":208},[178,1707,1708],{"class":188},"].id())\n",[178,1710,1711,1714,1716,1718,1720],{"class":180,"line":334},[178,1712,1713],{"class":188},"            project.removeEntry(",[178,1715,209],{"class":208},[178,1717,244],{"class":188},[178,1719,1652],{"class":215},[178,1721,255],{"class":188},[178,1723,1724,1726,1728,1730,1733],{"class":180,"line":358},[178,1725,633],{"class":184},[178,1727,1628],{"class":188},[178,1729,1631],{"class":184},[178,1731,1732],{"class":208}," SCHEMA_VERSION",[178,1734,645],{"class":188},[178,1736,1737,1740,1742,1744,1746,1748,1750],{"class":180,"line":382},[178,1738,1739],{"class":188},"        project.writeEntry(",[178,1741,209],{"class":208},[178,1743,244],{"class":188},[178,1745,1614],{"class":215},[178,1747,244],{"class":188},[178,1749,1578],{"class":208},[178,1751,255],{"class":188},[14,1753,1754,1756,1757,1760],{},[453,1755,455],{}," A stored schema number tells the plugin which layout of keys the project uses. Here version 1 stored the layer by name; the migration looks it up once, stores the id, and removes the old key. Each later change adds another ",[18,1758,1759],{},"if version \u003C n"," block, so a project can jump several versions in one load. Writing the new version marks the project modified, which reminds the user to save it so the migration is kept.",[142,1762,1764],{"id":1763},"test-project-settings-without-the-interface","Test project settings without the interface",[14,1766,1767,1768,1771],{},"Because entries live on the ",[18,1769,1770],{},"QgsProject"," object, tests can create a fresh project, write values, save to a temporary file, read it back and check the round trip — without a running QGIS window.",[169,1773,1775],{"className":171,"code":1774,"language":173,"meta":174,"style":174},"import tempfile, os\nfrom qgis.core import QgsProject\n\ndef test_round_trip():\n    p = QgsProject()\n    cfg = ProjectSettings(project=p)\n    cfg.tolerance_m = 2.5\n    path = os.path.join(tempfile.mkdtemp(), \"t.qgz\")\n    assert p.write(path)\n    q = QgsProject()\n    assert q.read(path)\n    assert ProjectSettings(project=q).tolerance_m == 2.5\n",[18,1776,1777,1784,1794,1798,1807,1817,1836,1846,1861,1869,1878,1885],{"__ignoreMap":174},[178,1778,1779,1781],{"class":180,"line":181},[178,1780,192],{"class":184},[178,1782,1783],{"class":188}," tempfile, os\n",[178,1785,1786,1788,1790,1792],{"class":180,"line":198},[178,1787,185],{"class":184},[178,1789,189],{"class":188},[178,1791,192],{"class":184},[178,1793,195],{"class":188},[178,1795,1796],{"class":180,"line":205},[178,1797,202],{"emptyLinePlaceholder":201},[178,1799,1800,1802,1805],{"class":180,"line":219},[178,1801,545],{"class":184},[178,1803,1804],{"class":548}," test_round_trip",[178,1806,581],{"class":188},[178,1808,1809,1812,1814],{"class":180,"line":231},[178,1810,1811],{"class":188},"    p ",[178,1813,225],{"class":184},[178,1815,1816],{"class":188}," QgsProject()\n",[178,1818,1819,1822,1824,1827,1831,1833],{"class":180,"line":236},[178,1820,1821],{"class":188},"    cfg ",[178,1823,225],{"class":184},[178,1825,1826],{"class":188}," ProjectSettings(",[178,1828,1830],{"class":1829},"s9osk","project",[178,1832,225],{"class":184},[178,1834,1835],{"class":188},"p)\n",[178,1837,1838,1841,1843],{"class":180,"line":258},[178,1839,1840],{"class":188},"    cfg.tolerance_m ",[178,1842,225],{"class":184},[178,1844,1845],{"class":208}," 2.5\n",[178,1847,1848,1851,1853,1856,1859],{"class":180,"line":278},[178,1849,1850],{"class":188},"    path ",[178,1852,225],{"class":184},[178,1854,1855],{"class":188}," os.path.join(tempfile.mkdtemp(), ",[178,1857,1858],{"class":215},"\"t.qgz\"",[178,1860,255],{"class":188},[178,1862,1863,1866],{"class":180,"line":298},[178,1864,1865],{"class":184},"    assert",[178,1867,1868],{"class":188}," p.write(path)\n",[178,1870,1871,1874,1876],{"class":180,"line":329},[178,1872,1873],{"class":188},"    q ",[178,1875,225],{"class":184},[178,1877,1816],{"class":188},[178,1879,1880,1882],{"class":180,"line":334},[178,1881,1865],{"class":184},[178,1883,1884],{"class":188}," q.read(path)\n",[178,1886,1887,1889,1891,1893,1895,1898,1901],{"class":180,"line":358},[178,1888,1865],{"class":184},[178,1890,1826],{"class":188},[178,1892,1830],{"class":1829},[178,1894,225],{"class":184},[178,1896,1897],{"class":188},"q).tolerance_m ",[178,1899,1900],{"class":184},"==",[178,1902,1845],{"class":208},[14,1904,1905,1907,1908,1911,1912,1915,1916,160],{},[453,1906,455],{}," Passing a project into ",[18,1909,1910],{},"ProjectSettings"," instead of always using ",[18,1913,1914],{},"QgsProject.instance()"," is what makes this testable. The test proves both that values are written with the right type and that they survive saving, which catches the commonest bug — writing an entry after the project was saved. Run it with the setup from ",[26,1917,1919],{"href":1918},"\u002Fqgis-plugin-development\u002Ftesting-and-ci-for-plugins\u002F","testing QGIS plugins with pytest",[142,1921,1923],{"id":1922},"qgis-version-compatibility","QGIS version compatibility",[14,1925,1926,1929,1930,1933,1934,1937,1938,1941],{},[18,1927,1928],{},"writeEntry",", the typed ",[18,1931,1932],{},"writeEntryDouble","\u002F",[18,1935,1936],{},"writeEntryBool"," and the corresponding read methods, and the project signals are available on QGIS 3.34 LTR, 3.40 LTR and QGIS 4. Project entries are stored in the project XML under ",[18,1939,1940],{},"properties",", so they survive across versions and are readable without the plugin installed.",[142,1943,1945],{"id":1944},"troubleshooting","Troubleshooting",[147,1947,1948,1954,1962,1968],{},[150,1949,1950,1953],{},[453,1951,1952],{},"Settings vanish when reopening the project."," The project was not saved after writing entries.",[150,1955,1956,1959,1960,160],{},[453,1957,1958],{},"The plugin uses the previous project's settings."," Settings were cached and not re-read on ",[18,1961,1089],{},[150,1963,1964,1967],{},[453,1965,1966],{},"A stored layer is not found."," It was removed, or the value stored is a name rather than an id.",[150,1969,1970,1973,1974,1977],{},[453,1971,1972],{},"Values come back as strings."," The untyped ",[18,1975,1976],{},"readEntry"," was used for numbers; use the typed readers.",[142,1979,1981],{"id":1980},"conclusion","Conclusion",[14,1983,1984,1985,1987,1988,1990,1991,1993,1994,1996],{},"Store project-specific configuration with ",[18,1986,1928],{}," under your plugin's scope and read it with typed readers, reference layers by id and handle removed layers, wrap settings in a small class with defaults, reload on ",[18,1989,1089],{}," and write pending state on ",[18,1992,1114],{},", and keep user preferences in ",[18,1995,79],{}," and per-layer configuration in layer properties.",[142,1998,2000],{"id":1999},"frequently-asked-questions","Frequently Asked Questions",[14,2002,2003,2006,2007,2010,2011,2013],{},[453,2004,2005],{},"Can I see project entries without the plugin?","\nYes — they are in the project XML; open the ",[18,2008,2009],{},".qgs"," inside the ",[18,2012,20],{}," to inspect them.",[14,2015,2016,2019],{},[453,2017,2018],{},"Do project entries work for projects stored in PostgreSQL?","\nYes; they are part of the project, wherever it is stored.",[14,2021,2022,2025,160],{},[453,2023,2024],{},"How do I remove an entry?",[18,2026,2027],{},"project.removeEntry(scope, key)",[14,2029,2030,2033],{},[453,2031,2032],{},"Can project variables replace entries?","\nFor values used in expressions, yes; for structured plugin configuration, entries are cleaner.",[142,2035,2037],{"id":2036},"related","Related",[147,2039,2040,2045,2050,2056,2062],{},[150,2041,2042,2044],{},[26,2043,29],{"href":28}," — the guide this recipe belongs to",[150,2046,2047],{},[26,2048,2049],{"href":158},"Store Plugin Settings with QgsSettings",[150,2051,2052],{},[26,2053,2055],{"href":2054},"\u002Fqgis-plugin-development\u002Fplugin-settings-and-localization\u002Fadd-plugin-options-page-to-qgis-settings\u002F","Add a Plugin Options Page to QGIS Settings",[150,2057,2058],{},[26,2059,2061],{"href":2060},"\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-projects\u002Fuse-project-variables-and-metadata-pyqgis\u002F","Use Project Variables and Metadata in PyQGIS",[150,2063,2064],{},[26,2065,2066],{"href":1552},"Add a Custom Layer Properties Page in PyQGIS",[2068,2069,2070],"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 .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 .sjoCn, html code.shiki .sjoCn{--shiki-default:#9AA79F}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}",{"title":174,"searchDepth":198,"depth":198,"links":2072},[2073,2074,2075,2076,2077,2078,2079,2080,2081,2082,2083,2084,2085],{"id":144,"depth":198,"text":145},{"id":163,"depth":198,"text":164},{"id":471,"depth":198,"text":472},{"id":718,"depth":198,"text":719},{"id":1060,"depth":198,"text":1061},{"id":1460,"depth":198,"text":1461},{"id":1561,"depth":198,"text":1562},{"id":1763,"depth":198,"text":1764},{"id":1922,"depth":198,"text":1923},{"id":1944,"depth":198,"text":1945},{"id":1980,"depth":198,"text":1981},{"id":1999,"depth":198,"text":2000},{"id":2036,"depth":198,"text":2037},"Save plugin configuration inside the QGIS project instead of the user profile — QgsProject writeEntry and readEntry, typed values and lists, layer references that survive renaming, reacting to project load and save, and choosing between project, user and layer settings.","md",{"slug":2089,"type":2090,"breadcrumb":2091,"datePublished":2092,"dateModified":2092},"store-per-project-plugin-settings-pyqgis","article","Store Per-Project Plugin Settings","2026-10-02","\u002Fqgis-plugin-development\u002Fplugin-settings-and-localization\u002Fstore-per-project-plugin-settings-pyqgis",{"title":5,"description":2086},"qgis-plugin-development\u002Fplugin-settings-and-localization\u002Fstore-per-project-plugin-settings-pyqgis\u002Findex","Ni5egQxqmIe_VzsNE3FJOtSvhAFtXv9EgsJ-kvRfJZ0",1790966257652]