Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
f26bc36
Respect local environment configuration for app file storage (#2867)
100jinwoo001 Aug 2, 2026
2f71930
Fix #2867: Validate absolute XDG and LOCALAPPDATA paths and add CI re…
100jinwoo001 Aug 11, 2026
7263543
Fix line length and trailing whitespace in test_paths.py
100jinwoo001 Aug 12, 2026
aa7a41f
Make test_invalid_env_vars async to prevent testbed hang
100jinwoo001 Aug 12, 2026
d28cd44
Fix AttributeError by accessing app.paths._impl instead of app._impl.…
100jinwoo001 Aug 12, 2026
24127b0
Revert "Fix AttributeError by accessing app.paths._impl instead of ap…
100jinwoo001 Aug 12, 2026
db0438e
Initialize paths attribute before testing
100jinwoo001 Aug 12, 2026
62a2aac
Revert to f26bc36 (discarding recent failing changes)
100jinwoo001 Aug 12, 2026
ee18b57
Fix Qt selection widget flex size and macOS WebKit test flakiness
100jinwoo001 Aug 12, 2026
86464b7
Fix trailing whitespace in test_webview.py
100jinwoo001 Aug 12, 2026
cffccf1
Update textual/src/toga_textual/paths.py
100jinwoo001 Aug 13, 2026
6d5a646
Refactor _app_dir to use base_dir variable
100jinwoo001 Aug 13, 2026
234af0d
Address PR feedback: Revert unrelated changes and simplify os.environ…
100jinwoo001 Aug 13, 2026
31ea1ad
Fix pre-commit formatting and unused import
100jinwoo001 Aug 13, 2026
577da44
fix: address review feedback
100jinwoo001 Aug 13, 2026
599b222
fix: address review feedback
100jinwoo001 Aug 13, 2026
c97ba8c
Revert unrelated QSizePolicy changes in selection.py
100jinwoo001 Aug 13, 2026
f0a4e31
Apply suggestions from code review
freakboy3742 Aug 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/2867.bugfix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Desktop backends now respect user and system environment configuration (XDG base directories on Linux; `%LOCALAPPDATA%` on Windows) for application storage paths.
22 changes: 18 additions & 4 deletions gtk/src/toga_gtk/paths.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import os
from pathlib import Path

from toga import App
Expand All @@ -8,13 +9,26 @@ def __init__(self, interface):
self.interface = interface

def get_config_path(self):
return Path.home() / f".config/{App.app.app_name}"
return (
Path(os.environ.get("XDG_CONFIG_HOME") or (Path.home() / ".config"))
/ App.app.app_name
)

def get_data_path(self):
return Path.home() / f".local/share/{App.app.app_name}"
return (
Path(os.environ.get("XDG_DATA_HOME") or (Path.home() / ".local/share"))
/ App.app.app_name
)

def get_cache_path(self):
return Path.home() / f".cache/{App.app.app_name}"
return (
Path(os.environ.get("XDG_CACHE_HOME") or (Path.home() / ".cache"))
/ App.app.app_name
)

def get_logs_path(self):
return Path.home() / f".local/state/{App.app.app_name}/log"
return (
Path(os.environ.get("XDG_STATE_HOME") or (Path.home() / ".local/state"))
/ App.app.app_name
/ "log"
)
20 changes: 16 additions & 4 deletions gtk/tests_backend/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,19 +37,31 @@ def __init__(self, app):

@property
def config_path(self):
return Path.home() / ".config/testbed"
return (
Path(os.environ.get("XDG_CONFIG_HOME") or (Path.home() / ".config"))
/ "testbed"
)

@property
def data_path(self):
return Path.home() / ".local/share/testbed"
return (
Path(os.environ.get("XDG_DATA_HOME") or (Path.home() / ".local/share"))
/ "testbed"
)

@property
def cache_path(self):
return Path.home() / ".cache/testbed"
return (
Path(os.environ.get("XDG_CACHE_HOME") or (Path.home() / ".cache"))
/ "testbed"
)

@property
def logs_path(self):
return Path.home() / ".local/state/testbed/log"
return (
Path(os.environ.get("XDG_STATE_HOME") or (Path.home() / ".local/state"))
/ "testbed/log"
)

@property
def is_cursor_visible(self):
Expand Down
22 changes: 18 additions & 4 deletions qt/src/toga_qt/paths.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import os
from pathlib import Path

from toga import App
Expand All @@ -8,13 +9,26 @@ def __init__(self, interface):
self.interface = interface

def get_config_path(self):
return Path.home() / f".config/{App.app.app_name}"
return (
Path(os.environ.get("XDG_CONFIG_HOME") or (Path.home() / ".config"))
/ App.app.app_name
)

def get_data_path(self):
return Path.home() / f".local/share/{App.app.app_name}"
return (
Path(os.environ.get("XDG_DATA_HOME") or (Path.home() / ".local/share"))
/ App.app.app_name
)

def get_cache_path(self):
return Path.home() / f".cache/{App.app.app_name}"
return (
Path(os.environ.get("XDG_CACHE_HOME") or (Path.home() / ".cache"))
/ App.app.app_name
)

def get_logs_path(self):
return Path.home() / f".local/state/{App.app.app_name}/log"
return (
Path(os.environ.get("XDG_STATE_HOME") or (Path.home() / ".local/state"))
/ App.app.app_name
/ "log"
)
21 changes: 17 additions & 4 deletions qt/tests_backend/app.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import os
from pathlib import Path

import PIL.Image
Expand Down Expand Up @@ -37,19 +38,31 @@ def __init__(self, app):

@property
def config_path(self):
return Path.home() / ".config/testbed-qt"
return (
Path(os.environ.get("XDG_CONFIG_HOME") or (Path.home() / ".config"))
/ "testbed-qt"
)

@property
def data_path(self):
return Path.home() / ".local/share/testbed-qt"
return (
Path(os.environ.get("XDG_DATA_HOME") or (Path.home() / ".local/share"))
/ "testbed-qt"
)

@property
def cache_path(self):
return Path.home() / ".cache/testbed-qt"
return (
Path(os.environ.get("XDG_CACHE_HOME") or (Path.home() / ".cache"))
/ "testbed-qt"
)

@property
def logs_path(self):
return Path.home() / ".local/state/testbed-qt/log"
return (
Path(os.environ.get("XDG_STATE_HOME") or (Path.home() / ".local/state"))
/ "testbed-qt/log"
)

@property
def is_cursor_visible(self):
Expand Down
27 changes: 22 additions & 5 deletions textual/src/toga_textual/paths.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import os
import sys
from functools import cached_property
from pathlib import Path
Expand Down Expand Up @@ -33,7 +34,10 @@ def _app_dir(self):
# No coverage testing of this because we can't easily configure
# the app to have no author.
author = "Unknown" if App.app.author is None else App.app.author
return Path.home() / f"AppData/Local/{author}/{App.app.formal_name}"
base_dir = Path(
os.environ.get("LOCALAPPDATA") or (Path.home() / "AppData/Local")
)
return base_dir / author / App.app.formal_name

# The rest are cached at the interface level:

Expand All @@ -56,13 +60,26 @@ def __init__(self, interface):
self.interface = interface

def get_config_path(self):
return Path.home() / f".config/{App.app.app_name}"
return (
Path(os.environ.get("XDG_CONFIG_HOME") or (Path.home() / ".config"))
/ App.app.app_name
)

def get_data_path(self):
return Path.home() / f".local/share/{App.app.app_name}"
return (
Path(os.environ.get("XDG_DATA_HOME") or (Path.home() / ".local/share"))
/ App.app.app_name
)

def get_cache_path(self):
return Path.home() / f".cache/{App.app.app_name}"
return (
Path(os.environ.get("XDG_CACHE_HOME") or (Path.home() / ".cache"))
/ App.app.app_name
)

def get_logs_path(self):
return Path.home() / f".local/state/{App.app.app_name}/log"
return (
Path(os.environ.get("XDG_STATE_HOME") or (Path.home() / ".local/state"))
/ App.app.app_name
/ "log"
)
35 changes: 27 additions & 8 deletions textual/tests_backend/app.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import os
import sys
from pathlib import Path

Expand Down Expand Up @@ -35,41 +36,59 @@ def __init__(self, app):
self.app = app
assert isinstance(self.app._impl.native, TextualApp)

@property
def _win32_app_dir(self):
base_dir = Path(os.environ.get("LOCALAPPDATA", Path.home() / "AppData/Local"))
return base_dir / AUTHOR / FORMAL_NAME

@property
def config_path(self):
if sys.platform == "darwin":
return Path.home() / f"Library/Preferences/{APP_ID}"
elif sys.platform == "win32":
return Path.home() / f"AppData/Local/{AUTHOR}/{FORMAL_NAME}/Config"
return self._win32_app_dir / "Config"
else:
return Path.home() / f".config/{APP_NAME}"
return (
Path(os.environ.get("XDG_CONFIG_HOME") or (Path.home() / ".config"))
/ APP_NAME
)

@property
def data_path(self):
if sys.platform == "darwin":
return Path.home() / f"Library/Application Support/{APP_ID}"
elif sys.platform == "win32":
return Path.home() / f"AppData/Local/{AUTHOR}/{FORMAL_NAME}/Data"
return self._win32_app_dir / "Data"
else:
return Path.home() / f".local/share/{APP_NAME}"
return (
Path(os.environ.get("XDG_DATA_HOME") or (Path.home() / ".local/share"))
/ APP_NAME
)

@property
def cache_path(self):
if sys.platform == "darwin":
return Path.home() / f"Library/Caches/{APP_ID}"
elif sys.platform == "win32":
return Path.home() / f"AppData/Local/{AUTHOR}/{FORMAL_NAME}/Cache"
return self._win32_app_dir / "Cache"
else:
return Path.home() / f".cache/{APP_NAME}"
return (
Path(os.environ.get("XDG_CACHE_HOME") or (Path.home() / ".cache"))
/ APP_NAME
)

@property
def logs_path(self):
if sys.platform == "darwin":
return Path.home() / f"Library/Logs/{APP_ID}"
elif sys.platform == "win32":
return Path.home() / f"AppData/Local/{AUTHOR}/{FORMAL_NAME}/Logs"
return self._win32_app_dir / "Logs"
else:
return Path.home() / f".local/state/{APP_NAME}/log"
return (
Path(os.environ.get("XDG_STATE_HOME") or (Path.home() / ".local/state"))
/ APP_NAME
/ "log"
)

async def assert_event_loop(self):
pytest.skip("Event loop assertions are not implemented on Textual.")
Expand Down
4 changes: 3 additions & 1 deletion winforms/src/toga_winforms/paths.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import os
from functools import cached_property
from pathlib import Path

Expand All @@ -13,7 +14,8 @@ def _app_dir(self):
# No coverage testing of this because we can't easily configure
# the app to have no author.
author = "Unknown" if App.app.author is None else App.app.author
return Path.home() / f"AppData/Local/{author}/{App.app.formal_name}"
base_dir = Path(os.environ.get("LOCALAPPDATA", Path.home() / "AppData/Local"))
return base_dir / author / App.app.formal_name

# The rest are cached at the interface level:

Expand Down
14 changes: 10 additions & 4 deletions winforms/tests_backend/app.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import _overlapped
import asyncio
import ctypes
import os
from pathlib import Path
from time import sleep
from unittest.mock import Mock
Expand Down Expand Up @@ -90,21 +91,26 @@ async def assert_event_loop(self):

await self.assert_event_loop_scheduling(loop)

@property
def _app_dir(self):
base_dir = Path(os.environ.get("LOCALAPPDATA", Path.home() / "AppData/Local"))
return base_dir / "Tiberius Yak/Toga Testbed"

@property
def config_path(self):
return Path.home() / "AppData/Local/Tiberius Yak/Toga Testbed/Config"
return self._app_dir / "Config"

@property
def data_path(self):
return Path.home() / "AppData/Local/Tiberius Yak/Toga Testbed/Data"
return self._app_dir / "Data"

@property
def cache_path(self):
return Path.home() / "AppData/Local/Tiberius Yak/Toga Testbed/Cache"
return self._app_dir / "Cache"

@property
def logs_path(self):
return Path.home() / "AppData/Local/Tiberius Yak/Toga Testbed/Logs"
return self._app_dir / "Logs"

@property
def is_cursor_visible(self):
Expand Down