Skip to content

Add user-space path anchors (desktop, documents, downloads, pictures) - #4645

Open
jenish0908 wants to merge 8 commits into
beeware:mainfrom
jenish0908:fix/3551-user-space-paths
Open

Add user-space path anchors (desktop, documents, downloads, pictures)#4645
jenish0908 wants to merge 8 commits into
beeware:mainfrom
jenish0908:fix/3551-user-space-paths

Conversation

@jenish0908

Copy link
Copy Markdown

Adds app.paths.desktop, app.paths.documents, app.paths.downloads and app.paths.pictures, implementing the scope agreed in the discussion on #3551:

  • The folders are never created by Toga; a RuntimeError is raised if the folder doesn't exist on the device.
  • macOS: fixed home-relative folder names (localization only affects how the names are displayed). The docs note that the system permission dialog appears automatically on first access, and that there is no API to request the permission in advance.
  • Windows: locations are obtained from the operating system (Environment.GetFolderPath), so folder redirection (e.g., by OneDrive) is honored. The Downloads folder isn't included in .NET's SpecialFolder enum, so it is obtained from the Win32 known folder API (SHGetKnownFolderPath), following the existing ctypes wrapper style in libs/.
  • Linux (GTK/Qt/Textual): locations honor the freedesktop xdg-user-dirs user-dirs.dirs configuration, using the same resolution order as platformdirs: the config file (located via XDG_CONFIG_HOME), then the matching XDG_*_DIR environment variable, then the default English folder name in the user's home folder.
  • Android / iOS / web: every getter raises RuntimeError, as these platforms don't have user-accessible user-space folders.
  • Dummy: clearly-fake locations under ~/toga-dummy, with a TOGA_DUMMY_USER_DIRS environment variable override so test suites stay inside a temporary location.
  • Testbed probes are added on every backend; the testbed test xfails on platforms without user-space folders. The test never deletes real user folders (it only removes a folder it created itself, empty, on minimal CI environments).

Fixes #3551

Validation

  • Core test suite passes locally (3,369 passed; toga/paths.py at 100% coverage, including branches).
  • pre-commit hooks (ruff check/format, codespell, rumdl) pass on the changed files.
  • Verified on a real Windows machine: the winforms and textual known-folder lookups return the correct redirect-aware paths, and the xdg-user-dirs parser was exercised across all of its resolution branches (customized entry, absolute path, environment variable, fallback).

PR Checklist:

  • I will abide by the BeeWare Code of Conduct
  • I have read and have followed the CONTRIBUTING.md file
  • This PR was generated or assisted using an AI tool

Assisted-by: Claude Fable 5

🤖 Generated with Claude Code

jenish0908 and others added 5 commits August 14, 2026 10:53
App paths now provide anchors for the user's Desktop, Documents,
Downloads and Pictures folders, as discussed in beeware#3551:

* The folders are never created; a RuntimeError is raised if the folder
  doesn't exist on the device.
* macOS uses the fixed home-relative folder names.
* Windows obtains the locations from the operating system, so folder
  redirection (e.g., by OneDrive) is honored. The Downloads folder isn't
  included in .NET's SpecialFolder enum, so it is obtained from the
  Win32 known folder API.
* Linux (GTK/Qt/Textual) honors the freedesktop xdg-user-dirs
  configuration, with the matching XDG_*_DIR environment variable and
  the default English folder names as fallbacks.
* Android, iOS and web raise RuntimeError, as these platforms don't
  have user-accessible user-space folders.
* The dummy backend uses a clearly-fake location (~/toga-dummy), with
  an environment variable override so test suites stay inside a
  temporary location.

Fixes beeware#3551

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…resolver

* Add 'freedesktop' and 'OneDrive' to the documentation spelling wordlist.
* Add a testbed test that exercises every branch of the xdg-user-dirs
  resolution (environment variable, fallback name, configuration file
  entry with $HOME expansion, and key missing from the configuration
  file) using a temporary XDG_CONFIG_HOME, so backend coverage is
  complete on CI machines that have no user-dirs.dirs configuration.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The fixture can't be constructed on Android (there is no username, so
pytest can't create its temporary root), and fixture setup runs before
the test's skip. Use a manually created temporary directory after the
skip instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@freakboy3742 freakboy3742 left a comment

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.

Thanks for the PR.

The general shape of this looks on the right track; my main concern is the duplication of logic. There's a lot of reproduced implementations of XDG paths handling, and 2 different implementations of Windows path handling (one using ctypes, one using Python.net)

The original discussion mentioned platformdirs as an alternate implementation, but leaned away from using it on the basis that the implementation wasn't that complicated. Looking at this code, that clearly isn't the case. I think we should reconsider that decision.

I think the best approach here will be to:

  1. Modify toga-core so that it looks for a paths module on the backend. If that module exists, defer to that backend implementation.
  2. If there's no paths module, fall back to the a platformdirs-based implementation
  3. Add platformdirs as a dependency of toga-gtk, toga-qt, toga-winforms, and toga-textual.

In the core, we'll need a way to move the dummy into a "no paths module" configuration so that we can test both options.

In the testbed, the probes shouldn't use platformdirs. They shouldn't reproduce platformdirs logic; they should only have enough logic so that the actual paths as observed in CI can be asserted, and maybe some common user-space configuration paths.

## Notes

- On macOS, the operating system may show a permission dialog the first time the app accesses the user's Desktop, Documents, Downloads or Pictures folder. This happens automatically on first file access; there is no API to request the permission in advance. If the user denies access, the path can still be obtained, but file operations in the folder will fail.
- On Linux, the user-space folder locations honor the `user-dirs.dirs` configuration created by the freedesktop `xdg-user-dirs` tool (folder names differ between languages). If there is no configuration for a folder, the matching `XDG_*_DIR` environment variable is used, falling back to the default English folder name in the user's home folder.

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.

Suggested change
- On Linux, the user-space folder locations honor the `user-dirs.dirs` configuration created by the freedesktop `xdg-user-dirs` tool (folder names differ between languages). If there is no configuration for a folder, the matching `XDG_*_DIR` environment variable is used, falling back to the default English folder name in the user's home folder.
- On Linux, the user-space folder locations honor the [freedesktop.org XDG Base Directory Specification](https://specifications.freedesktop.org/basedir/latest/). If there is no configuration, the location will fall back to the default English folder name in the user's home folder.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Thanks for the detailed direction. agreed, the duplication was getting out of
hand. I've reworked it as you described.

-> toga-core now asks the factory for a Paths implementation, and falls back to a platformdirs-based implementation.
-> GTK, Qt, WinForms and Textual no longer have a paths module at all , and declare platformdirs >= 4.1 as a dependency
->Cocoa, Android, iOS, web and dummy keep their own implementations.
-> For testing both modes the dummy still provides Paths, and a test fixture hides the dummy's Paths entry point from the factory to exercise the platformdirs fallback.
-> Testbed probes no longer contain any resolution logic just the literal paths expected on CI.

Comment thread dummy/src/toga_dummy/paths.py Outdated
@@ -18,3 +19,26 @@ def get_cache_path(self):

def get_logs_path(self):
return Path.home() / f"logs/{App.app.app_id}"

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.

If we're adding "TOGA_DUMMY_USER_DIRS", we should use it throughout the dummy backend, not just on the new endpoints.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done, the dummy now routes all its paths through a single root. TOGA_DUMMY_HOME if set, otherwise ~/toga-dummy. Renamed the variable since it's no longer just for user dirs.

Comment thread dummy/src/toga_dummy/paths.py Outdated
Comment on lines +24 to +27
# User-space folders are returned as clearly dummy locations in the
# user's home folder. A test suite can redirect them to a temporary
# location by setting the TOGA_DUMMY_USER_DIRS environment variable,
# ensuring tests can't accidentally touch a real permanent location.

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.

Claude has a habit of being verbose. We don't need that in our codebase.

Suggested change
# User-space folders are returned as clearly dummy locations in the
# user's home folder. A test suite can redirect them to a temporary
# location by setting the TOGA_DUMMY_USER_DIRS environment variable,
# ensuring tests can't accidentally touch a real permanent location.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Removed, Also trimmed other comments.

Comment thread gtk/src/toga_gtk/paths.py Outdated
def _xdg_user_dir(name, fallback):
"""Resolve the location of an XDG user directory.
Locations follow the freedesktop ``xdg-user-dirs`` tool: the

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.

We use Markdown, not ReStructured text.

Suggested change
Locations follow the freedesktop ``xdg-user-dirs`` tool: the
Locations follow the freedesktop `xdg-user-dirs` tool: the

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed.

Comment thread gtk/src/toga_gtk/paths.py Outdated


def _xdg_user_dir(name, fallback):
"""Resolve the location of an XDG user directory.

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.

There will be some overlap with the changes from #4606.

Since these changes are (a) non-trivial, (b) required by multiple backends (GTK, Qt, and Textual), it would be worth factoring this "XDG config parse" into toga-core as a utility method that backends can use if they want.

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.

The other option would be to investigate how easy it would be to lean on platformdirs, rather than rebuilding this ourselves. platformdirs won't work on iOS, Android or web (at least, not at present); but it should be possible to use it on desktop platforms, and use the Toga API as a convenient way to get the PlatformDirs object that is pre-configured for the app being executed.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Chose the platformdirs option, per your main review the hand-rolled XDG parser is gone entirely, so there's no longer any overlap with #4606.
The fallback in core builds a PlatformDirs object pre-configured from the running app (appname=app_name, appauthor=author), and GTK/Qt/WinForms/Textual just rely on it, iOS, Android and web keep their own implementations since platformdirs doesn't support them.

Comment thread core/tests/test_paths.py Outdated
@pytest.mark.parametrize("name", ["desktop", "documents", "downloads", "pictures"])
def test_user_dir(app, name, tmp_path, monkeypatch):
"""User-space paths return the location provided by the backend."""
monkeypatch.setenv("TOGA_DUMMY_USER_DIRS", str(tmp_path))

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.

We need to ensure this is the default behavior of the test suite; so this should be part of the app fixture, rather than being manually included everywhere.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done, the app fixture now sets TOGA_DUMMY_HOME to the test's tmp_path, so every core test is contained in a temporary directory by default, and the individual tests no longer set it manually.

Comment thread testbed/tests/test_paths.py Outdated
expected.rmdir()


async def test_xdg_user_dir_resolution(app_probe, monkeypatch):

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.

This should be a parameterised test, not one test generating 4 outcomes.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

That test is gone entirely with resolution handled by platformdirs, the testbed no longer needs to exercise it and per your note the probes now only carry literal expected paths

…entation

Addresses review feedback on beeware#4645:

* toga-core now falls back to a platformdirs-based Paths implementation
  when the backend doesn't provide one.
* The GTK, Qt, WinForms and Textual backends no longer provide a paths
  module; they depend on platformdirs >= 4.1 instead. This removes the
  duplicated XDG parsing and the two separate Windows implementations.
* The dummy backend routes all paths through a single root, overridable
  with TOGA_DUMMY_HOME; the core test app fixture sets it to the test's
  tmp_path by default.
* Core tests cover the fallback by hiding the dummy's Paths entry point
  and stubbing PlatformDirs.
* Testbed probes now only contain literal expected paths.
* Trimmed comments, and switched docstrings to Markdown formatting.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@freakboy3742 freakboy3742 left a comment

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.

I haven't done a full review yet, but two things stand out:

  1. There's a lot of merge conflicts that need resolving
  2. Is there any reason to exclude macOS from the platformdirs-based implementation?
  3. There should be essentially no changes to the existing tesbed tests for pre-existing paths. If there are, then we've got a change in behavior. That represents a backward-incompatible change, which should be avoided if at all possible.

@jenish0908

jenish0908 commented Aug 17, 2026

Copy link
Copy Markdown
Author
  1. I'll resolve the conflicts.
  2. I left macOS out on purpose because platformdirs doesn't match where Toga currently puts things on macOS. Right now config goes in ~/Library/Preferences/<app_id> and data in ~/Library/Application Support/<app_id>, but platformdirs would put both in ~/Library/Application Support/<app_name>. So any existing app that upgraded Toga would stop finding its own config and data which is the same backward-incompatible change you're flagging in your third point. That's why I kept cocoa on its own implementation.
  3. Agreed, I'll rework it so the existing app-specific paths don't change at all. Also GTK/Qt would stay on the platformdirs fallback since its output there is identical to the current paths. Happy to revert those if you prefer.

jenish0908 and others added 2 commits August 17, 2026 14:37
Merges origin/main (including beeware#4606's environment-aware app paths) and
reworks the PR so pre-existing paths don't change:

* WinForms and Textual restore their app-specific path implementations,
  and use platformdirs only for the new user-space folders.
* GTK and Qt stay on the platformdirs fallback; its output is identical
  to the app path behavior introduced by beeware#4606.
* Testbed probes revert to the existing app path expectations.
* The backward-incompatible change note is no longer needed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add path shortcuts for common user-space paths

2 participants