Skip to content

Commit 08de6cc

Browse files
committed
feat: enforce default profile in QGIS Project file association in Windows registry
1 parent 87ee129 commit 08de6cc

File tree

6 files changed

+285
-23
lines changed

6 files changed

+285
-23
lines changed

docs/jobs/default_profile_setter.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,18 @@ Sample job configuration in your scenario file:
1616
force_profile_selection_policy: true
1717
```
1818
19+
On Windows, if you want to ensure that double-clicking on QGIS Project files will launch QGIS with the chosen default profile and disable version checking:
20+
21+
```yaml
22+
- name: Set default profile to conf_qgis_fr
23+
uses: default-profile-setter
24+
with:
25+
profile: conf_qgis_fr
26+
force_profile_selection_policy: true
27+
force_profile_file_association: true
28+
profile_file_association_arguments: "--noversioncheck"
29+
```
30+
1931
----
2032
2133
## Options
@@ -29,6 +41,22 @@ Name of the profile to set as default profile.
2941
Force the key `selectionPolicy` to 1, which will always open the profile defined in the `defaultProfile` key in `profiles.ini` file. In this context, this job will force QGIS to always start with the profile specified in this job.
3042
It's the same behavior as [the option _Always use profile_ in QGIS user profiles preferences](https://docs.qgis.org/latest/en/docs/user_manual/introduction/qgis_configuration.html#setting-user-profile).
3143

44+
### force_profile_file_association
45+
46+
:::{note}
47+
Windows-only feature
48+
:::
49+
50+
Modify the Windows registry to ensure that QGIS Project files always open with the default profile when launched.
51+
52+
### profile_file_association_arguments
53+
54+
:::{note}
55+
Windows-only feature
56+
:::
57+
58+
Arguments to pass to QGIS executable.
59+
3260
----
3361

3462
## Schema

docs/schemas/scenario/jobs/default-profile-setter.json

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,24 @@
1313
"description": "Force the profile selection policy to 1 (always open default profile).",
1414
"type": "boolean",
1515
"default": false
16+
},
17+
"force_profile_file_association": {
18+
"description": "Force the QGIS project file association to starts QGIS with the default profile.",
19+
"type": "boolean",
20+
"default": false
21+
},
22+
"profile_file_association_arguments": {
23+
"description": "Arguments to pass to QGIS executable when force_profile_file_association is true.",
24+
"type": "string",
25+
"example": "--noversioncheck"
26+
},
27+
"force_registry_key_creation": {
28+
"description": "Force the creation of the registry key for the default profile. This is useful when QGIS is not installed yet.",
29+
"type": "boolean",
30+
"default": false
1631
}
17-
}
32+
},
33+
"required": [
34+
"profile"
35+
]
1836
}

qgis_deployment_toolbelt/jobs/job_default_profile_setter.py

Lines changed: 65 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@
1313

1414
# Standard library
1515
import logging
16+
from sys import platform as opersys
1617

1718
# package
1819
from qgis_deployment_toolbelt.jobs.generic_job import GenericJob
1920
from qgis_deployment_toolbelt.utils.ini_parser_with_path import CustomConfigParser
21+
from qgis_deployment_toolbelt.utils.win32utils import set_qgis_command
2022

2123
# #############################################################################
2224
# ########## Globals ###############
@@ -52,6 +54,29 @@ class JobDefaultProfileSetter(GenericJob):
5254
"possible_values": None,
5355
"condition": None,
5456
},
57+
"force_profile_file_association": {
58+
"type": bool,
59+
"required": False,
60+
"default": False,
61+
"possible_values": None,
62+
"condition": None,
63+
},
64+
"profile_file_association_arguments": {
65+
"type": str,
66+
"required": False,
67+
"default": None,
68+
"possible_values": None,
69+
"condition": lambda options: options.get("force_profile_file_association")
70+
is True,
71+
},
72+
"force_registry_key_creation": {
73+
"type": bool,
74+
"required": False,
75+
"default": False,
76+
"possible_values": None,
77+
"condition": lambda options: options.get("force_profile_file_association")
78+
is True,
79+
},
5580
}
5681

5782
def __init__(self, options: dict) -> None:
@@ -81,32 +106,61 @@ def run(self) -> None:
81106
logger.error("No QGIS profile matching the provided profile name.")
82107
return
83108

84-
ini_profiles_path = self.qgis_profiles_path / "profiles.ini"
109+
self.ini_profiles_path = self.qgis_profiles_path / "profiles.ini"
85110

86111
# check if the profiles.ini file exists and create it with default profile set
87112
# if not
88-
if not ini_profiles_path.exists():
89-
logger.warning(
90-
"Configuration file profiles.ini doesn't exist. "
91-
"It will be created but maybe it was not the expected behavior."
113+
if not self.ini_profiles_path.exists():
114+
self._create_ini_profiles()
115+
else:
116+
self._alter_ini_profiles()
117+
118+
if self.options.get("force_profile_file_association"):
119+
qgis_cmd = (
120+
f'"{self.os_config.get_qgis_bin_path}" --profile "{qdt_profile.name}"'
92121
)
93-
ini_profiles_path.touch(exist_ok=True)
122+
if self.options.get("profile_file_association_arguments"):
123+
qgis_cmd += f' {self.options.get("profile_file_association_arguments")}'
124+
qgis_cmd += ' "%1"'
125+
logger.debug(f"Command to set file association: {qgis_cmd}")
126+
if opersys == "win32":
127+
command_setted = set_qgis_command(
128+
qgis_cmd=qgis_cmd,
129+
force_key_creation=self.options.get(
130+
"force_registry_key_creation", False
131+
),
132+
)
133+
if not command_setted:
134+
logger.error(
135+
"Failed to set file association for the default profile."
136+
)
137+
else:
138+
logger.warning(
139+
"File association is only supported on Windows. "
140+
"Skipping file association setting."
141+
)
142+
143+
logger.debug(f"Job {self.ID} ran successfully.")
144+
145+
def _create_ini_profiles(self) -> None:
146+
"""Create the profiles.ini file with the default profile."""
147+
if not self.ini_profiles_path.exists():
148+
self.ini_profiles_path.touch(exist_ok=True)
94149
data = f"[core]\ndefaultProfile={self.options.get('profile')}"
95150
if self.options.get("force_profile_selection_policy"):
96151
data += "\nselectionPolicy=1"
97-
ini_profiles_path.write_text(
152+
self.ini_profiles_path.write_text(
98153
data=data,
99154
encoding="UTF8",
100155
)
101156
logger.info(f"Default profile set to {self.options.get('profile')}")
102-
logger.debug(f"Job {self.ID} ran successfully.")
103-
return
104157

158+
def _alter_ini_profiles(self) -> None:
159+
"""Alter the ini profiles with the default profile."""
105160
ini_profiles = CustomConfigParser()
106161
ini_profiles.optionxform = str
107162
ini_profiles.read(self.qgis_profiles_path / "profiles.ini", encoding="UTF8")
108163

109-
# set the default profile
110164
if not ini_profiles.has_section("core"):
111165
ini_profiles.add_section("core")
112166

@@ -122,8 +176,6 @@ def run(self) -> None:
122176
)
123177
ini_profiles.set("core", "selectionPolicy", "1")
124178

125-
with ini_profiles_path.open("w", encoding="UTF8") as wf:
179+
with self.ini_profiles_path.open("w", encoding="UTF8") as wf:
126180
ini_profiles.write(wf, space_around_delimiters=False)
127181
logger.info(f"Default profile set to {self.options.get('profile')}")
128-
129-
logger.debug(f"Job {self.ID} ran successfully.")

qgis_deployment_toolbelt/utils/win32utils.py

Lines changed: 84 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,19 @@
3939

4040
if opersys == "win32":
4141
"""windows"""
42-
system_hkey = (
42+
env_system_hkey = (
4343
winreg.HKEY_LOCAL_MACHINE,
4444
r"SYSTEM\CurrentControlSet\Control\Session Manager\Environment",
4545
)
46-
user_hkey = (winreg.HKEY_CURRENT_USER, r"Environment")
46+
env_user_hkey = (winreg.HKEY_CURRENT_USER, r"Environment")
47+
qgis_command_system_hkey = (
48+
winreg.HKEY_LOCAL_MACHINE,
49+
r"QGIS Project\Shell\open\command",
50+
)
51+
qgis_command_user_hkey = (
52+
winreg.HKEY_CURRENT_USER,
53+
r"Software\Classes\QGIS Project\Shell\open\command",
54+
)
4755

4856

4957
# #############################################################################
@@ -115,9 +123,9 @@ def delete_environment_variable(envvar_name: str, scope: str = "user") -> bool:
115123
"""
116124
# user or system
117125
if scope == "user":
118-
hkey = user_hkey
126+
hkey = env_user_hkey
119127
else:
120-
hkey = system_hkey
128+
hkey = env_system_hkey
121129

122130
# get it to check if variable exits
123131
try:
@@ -150,9 +158,9 @@ def get_environment_variable(envvar_name: str, scope: str = "user") -> str | Non
150158
"""
151159
# user or system
152160
if scope == "user":
153-
hkey = user_hkey
161+
hkey = env_user_hkey
154162
else:
155-
hkey = system_hkey
163+
hkey = env_system_hkey
156164

157165
# try to get the value
158166
try:
@@ -281,9 +289,9 @@ def set_environment_variable(
281289
"""
282290
# user or system
283291
if scope == "user":
284-
hkey = user_hkey
292+
hkey = env_user_hkey
285293
else:
286-
hkey = system_hkey
294+
hkey = env_system_hkey
287295

288296
# try to set the value
289297
try:
@@ -298,6 +306,74 @@ def set_environment_variable(
298306
return False
299307

300308

309+
def read_registry_value(
310+
key: tuple, value_name: str, access: int = winreg.KEY_READ
311+
) -> str | None:
312+
r"""Read a value from the Windows registry.
313+
Args:
314+
key (tuple): registry key to read from, e.g. (winreg.HKEY_CURRENT_USER, r"Software\Classes\QGIS Project\Shell\open\command")
315+
value_name (str): name of the value to read
316+
access (int, optional): access mode for the registry key, defaults to winreg.KEY_READ
317+
Returns:
318+
str | None: the value as a string if found, None if not found or an error occurs
319+
"""
320+
try:
321+
with winreg.OpenKey(*key, access=access) as reg_key:
322+
value, _ = winreg.QueryValueEx(reg_key, value_name)
323+
return value
324+
except FileNotFoundError:
325+
logger.error(f"Registry key {key} or value {value_name} not found.")
326+
return None
327+
except OSError as err:
328+
logger.error(f"Error reading registry key {key}: {err}")
329+
return None
330+
331+
332+
def set_qgis_command(
333+
qgis_cmd: str, scope: str = "user", force_key_creation: bool = False
334+
) -> bool:
335+
"""Set QGIS command in Windows registry.
336+
337+
Args:
338+
qgis_path (str): path to QGIS installation folder
339+
scope (str, optional): environment variable scope. Must be "user" or "system",
340+
defaults to "user". Defaults to "user".
341+
342+
Returns:
343+
bool: True is the variable has been successfully set
344+
"""
345+
# user or system
346+
if scope == "user":
347+
hkey = qgis_command_user_hkey
348+
else:
349+
hkey = qgis_command_system_hkey
350+
351+
if force_key_creation:
352+
# ensure the key exists
353+
try:
354+
with winreg.CreateKeyEx(*hkey, access=winreg.KEY_WRITE) as key:
355+
pass # just create the key if it does not exist
356+
except OSError as err:
357+
logger.error(
358+
f"Create QGIS command registry key for scope '{scope}' failed. Trace: {err}"
359+
)
360+
return False
361+
362+
# try to set the value
363+
try:
364+
with winreg.OpenKey(*hkey, access=winreg.KEY_WRITE) as key:
365+
winreg.SetValueEx(key, "", 0, winreg.REG_SZ, qgis_cmd)
366+
return True
367+
except FileNotFoundError:
368+
logger.error(f"Registry key {hkey} not found. Is QGIS installed?")
369+
return False
370+
except OSError as err:
371+
logger.error(
372+
f"Set QGIS command '{qgis_cmd}' to scope '{scope}' failed. Trace: {err}"
373+
)
374+
return False
375+
376+
301377
# #############################################################################
302378
# ##### Stand alone program ########
303379
# ##################################

tests/test_job_default_profile_setter.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,18 @@
1717
# Standard library
1818
import tempfile
1919
import unittest
20+
from os import environ, getenv
2021
from pathlib import Path
22+
from sys import platform as opersys
2123

2224
# package
2325
from qgis_deployment_toolbelt.jobs.job_default_profile_setter import (
2426
JobDefaultProfileSetter,
2527
)
28+
from qgis_deployment_toolbelt.utils.win32utils import (
29+
qgis_command_user_hkey,
30+
read_registry_value,
31+
)
2632

2733

2834
# #############################################################################
@@ -115,3 +121,50 @@ def test_job_default_profile_setter_run_with_force_profile_selection_policy(self
115121
"[core]\ndefaultProfile=qdt_test_profile_minimal\nselectionPolicy=1\n\n"
116122
)
117123
self.assertEqual(content, expected_content)
124+
125+
@unittest.skipUnless(
126+
opersys == "win32" and getenv("GITHUB_ACTIONS", False),
127+
"Test specific to Windows and run only in CI to avoid modifying registry.",
128+
)
129+
def test_job_default_profile_setter_run_with_force_profile_file_association(self):
130+
"""Run the job with force profile file association."""
131+
self.default_profile_setter_job.options["force_profile_file_association"] = True
132+
self.default_profile_setter_job.options["force_registry_key_creation"] = True
133+
environ["QDT_QGIS_EXE_PATH"] = "/usr/bin/toto"
134+
135+
self.default_profile_setter_job.run()
136+
self.assertTrue(self.profiles_ini.exists())
137+
138+
reg_value = read_registry_value(
139+
qgis_command_user_hkey,
140+
"",
141+
)
142+
expected_reg_value = (
143+
'"\\usr\\bin\\toto" --profile "qdt_test_profile_minimal" "%1"'
144+
)
145+
self.assertEqual(reg_value, expected_reg_value)
146+
147+
@unittest.skipUnless(
148+
opersys == "win32" and getenv("GITHUB_ACTIONS", False),
149+
"Test specific to Windows and run only in CI to avoid modifying registry.",
150+
)
151+
def test_job_default_profile_setter_run_with_profile_file_association_arguments(
152+
self,
153+
):
154+
"""Run the job with force profile file association."""
155+
self.default_profile_setter_job.options["force_profile_file_association"] = True
156+
self.default_profile_setter_job.options["force_registry_key_creation"] = True
157+
self.default_profile_setter_job.options[
158+
"profile_file_association_arguments"
159+
] = "--noversioncheck"
160+
environ["QDT_QGIS_EXE_PATH"] = "/usr/bin/toto"
161+
162+
self.default_profile_setter_job.run()
163+
self.assertTrue(self.profiles_ini.exists())
164+
165+
reg_value = read_registry_value(
166+
qgis_command_user_hkey,
167+
"",
168+
)
169+
expected_reg_value = '"\\usr\\bin\\toto" --profile "qdt_test_profile_minimal" --noversioncheck "%1"'
170+
self.assertEqual(reg_value, expected_reg_value)

0 commit comments

Comments
 (0)