Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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_CONFIG_HOME`, `XDG_DATA_HOME`, `XDG_CACHE_HOME`, and `XDG_STATE_HOME` on Linux; `%LOCALAPPDATA%` on Windows) for application storage paths.
1 change: 0 additions & 1 deletion core/tests/test_paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,5 @@ def test_subclassed_as_deep_module():
)
def test_cant_reassign(app, path_name):
"""App path attributes are read-only."""
# Theoretically, this could leak out of this test... but only if it fails!

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please don't remove comments that are still accurate, correct, and unrelated to the changes that you're making.

with pytest.raises(AttributeError):
setattr(app.paths, path_name, "")
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
30 changes: 25 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,13 @@ 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}"
local_app_data = os.environ.get("LOCALAPPDATA")
base_dir = (
Path(local_app_data)
if local_app_data
else (Path.home() / "AppData/Local")
)
Comment thread
100jinwoo001 marked this conversation as resolved.
Outdated
return base_dir / author / App.app.formal_name

# The rest are cached at the interface level:

Expand All @@ -56,13 +63,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"
)
38 changes: 30 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,62 @@ def __init__(self, app):
self.app = app
assert isinstance(self.app._impl.native, TextualApp)

@property
def _win32_app_dir(self):
local_app_data = os.environ.get("LOCALAPPDATA")
base_dir = (
Path(local_app_data) if local_app_data else (Path.home() / "AppData/Local")
)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, os.environ.get() has a fallback mode.

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
7 changes: 6 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,11 @@ 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}"
local_app_data = os.environ.get("LOCALAPPDATA")
base_dir = (
Path(local_app_data) if local_app_data else (Path.home() / "AppData/Local")
)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same again - fallback mode can be used to simplify things.

return base_dir / author / App.app.formal_name

# The rest are cached at the interface level:

Expand Down
17 changes: 13 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,29 @@ async def assert_event_loop(self):

await self.assert_event_loop_scheduling(loop)

@property
def _app_dir(self):
local_app_data = os.environ.get("LOCALAPPDATA")
base_dir = (
Path(local_app_data) if local_app_data else (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
Loading