Skip to content

Commit 31e5ad5

Browse files
committed
Make the GTK backend respect XDG app path environment variables.
This commit also updates related documentation.
1 parent 7c165cf commit 31e5ad5

File tree

5 files changed

+95
-13
lines changed

5 files changed

+95
-13
lines changed

changes/3482.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
The GTK backend now respects XDG app path environment variables.

changes/3482.removal.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
The GTK backend now respects XDG app path environment variables, which means the places it expects to find app data might change in environments where those variables were previously set and being ignored. You'll have to manage that transition; Toga will not move any data itself.

docs/reference/api/resources/app_paths.rst

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ location to which they can be considered relative.
2323
Complicating matters further, operating systems have conventions (and in some
2424
cases, hard restrictions) over where certain file types should be stored. For
2525
example, macOS provides the ``~/Library/Application Support`` folder; Linux
26-
encourages use of the ``~/.config`` folder (amongst others), and Windows
26+
encourages use of the ``~/.config`` folder (amongst others),
27+
while allowing users to override the defaults; and Windows
2728
provides the ``AppData/Local`` folder in the user's home directory. Application
2829
sandbox and security policies will sometimes prevent reading or
2930
writing files in any location other than these pre-approved locations.
@@ -43,6 +44,16 @@ up you to create any desired subdirectory - if you want to create a
4344
``apps.path.config`` will exist, but you must take responsibility for
4445
creating the ``credentials`` subdirectory before saving ``user.toml``.
4546

47+
Notes
48+
-----
49+
50+
* The GTK backend partially implements the
51+
`XDG Base Directory Specification <https://specifications.freedesktop.org/basedir-spec/latest/>`__;
52+
specifically, it uses the default XDG paths for data, config, state, and cache
53+
directories, and respects the environment variables that allow overriding the
54+
default paths. Per the specification, the override paths must be absolute; relative
55+
paths will be ignored.
56+
4657
Reference
4758
---------
4859

gtk/src/toga_gtk/paths.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import os
12
from pathlib import Path
23

34
from toga import App
@@ -7,14 +8,28 @@ class Paths:
78
def __init__(self, interface):
89
self.interface = interface
910

11+
def _get_root(self, envvar, default):
12+
custom_root_raw = os.getenv(envvar)
13+
custom_root = Path(custom_root_raw) if custom_root_raw else None
14+
15+
# The XDG Base Directory spec requires paths to be absolute
16+
if custom_root and custom_root.is_absolute():
17+
return custom_root
18+
19+
return Path.home() / default
20+
1021
def get_config_path(self):
11-
return Path.home() / f".config/{App.app.app_name}"
22+
root = self._get_root("XDG_CONFIG_HOME", ".config")
23+
return root / App.app.app_name
1224

1325
def get_data_path(self):
14-
return Path.home() / f".local/share/{App.app.app_name}"
26+
root = self._get_root("XDG_DATA_HOME", ".local/share")
27+
return root / App.app.app_name
1528

1629
def get_cache_path(self):
17-
return Path.home() / f".cache/{App.app.app_name}"
30+
root = self._get_root("XDG_CACHE_HOME", ".cache")
31+
return root / App.app.app_name
1832

1933
def get_logs_path(self):
20-
return Path.home() / f".local/state/{App.app.app_name}/log"
34+
root = self._get_root("XDG_STATE_HOME", ".local/state")
35+
return root / App.app.app_name / "log"

gtk/tests_backend/app.py

Lines changed: 62 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import os
2+
import tempfile
23
from contextlib import contextmanager
34
from pathlib import Path
45

@@ -32,16 +33,69 @@ def is_cursor_visible(self):
3233

3334
@contextmanager
3435
def prepare_paths(self, *, custom):
35-
if custom:
36-
pytest.xfail("This backend doesn't implement app path customization.")
37-
38-
yield {
39-
"config": Path.home() / ".config/testbed",
40-
"data": Path.home() / ".local/share/testbed",
41-
"cache": Path.home() / ".cache/testbed",
42-
"logs": Path.home() / ".local/state/testbed/log",
36+
# Backup environment variables for later restoration
37+
backup = {
38+
"XDG_CONFIG_HOME": os.getenv("XDG_CONFIG_HOME"),
39+
"XDG_DATA_HOME": os.getenv("XDG_DATA_HOME"),
40+
"XDG_CACHE_HOME": os.getenv("XDG_CACHE_HOME"),
41+
"XDG_STATE_HOME": os.getenv("XDG_STATE_HOME"),
4342
}
4443

44+
# Creating this variable here so it can be checked during cleanup
45+
temp_custom_dir = None
46+
47+
try:
48+
if custom:
49+
# This will be cleaned up later
50+
temp_custom_dir = tempfile.TemporaryDirectory()
51+
52+
custom_root = Path(temp_custom_dir.name)
53+
app_paths = {
54+
"config": custom_root / "config",
55+
"data": custom_root / "data",
56+
"cache": custom_root / "cache",
57+
"state": custom_root / "state",
58+
}
59+
60+
# Set the custom paths
61+
os.environ["XDG_CONFIG_HOME"] = str(app_paths["config"])
62+
os.environ["XDG_DATA_HOME"] = str(app_paths["data"])
63+
os.environ["XDG_CACHE_HOME"] = str(app_paths["cache"])
64+
os.environ["XDG_STATE_HOME"] = str(app_paths["state"])
65+
else:
66+
# Delete existing environment variables to replicate the
67+
# default state.
68+
for envvar in backup:
69+
if envvar in os.environ:
70+
del os.environ[envvar]
71+
72+
# The default paths
73+
app_paths = {
74+
"config": Path.home() / ".config",
75+
"data": Path.home() / ".local/share",
76+
"cache": Path.home() / ".cache",
77+
"state": Path.home() / ".local/state",
78+
}
79+
80+
yield {
81+
"config": app_paths["config"] / "testbed",
82+
"data": app_paths["data"] / "testbed",
83+
"cache": app_paths["cache"] / "testbed",
84+
"logs": app_paths["state"] / "testbed" / "log",
85+
}
86+
finally:
87+
# Restore environment variables
88+
for envvar, value in backup.items():
89+
if value is not None:
90+
os.environ[envvar] = value
91+
else:
92+
if envvar in os.environ:
93+
del os.environ[envvar]
94+
95+
# Clean up temporary custom directory if it was created
96+
if temp_custom_dir:
97+
temp_custom_dir.cleanup()
98+
4599
def unhide(self):
46100
pytest.xfail("This platform doesn't have an app level unhide.")
47101

0 commit comments

Comments
 (0)