Add user-space path anchors (desktop, documents, downloads, pictures) - #4645
Add user-space path anchors (desktop, documents, downloads, pictures)#4645jenish0908 wants to merge 8 commits into
Conversation
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
left a comment
There was a problem hiding this comment.
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:
- Modify toga-core so that it looks for a
pathsmodule on the backend. If that module exists, defer to that backend implementation. - If there's no
pathsmodule, fall back to the aplatformdirs-based implementation - Add
platformdirsas 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. |
There was a problem hiding this comment.
| - 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. |
There was a problem hiding this comment.
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.
| @@ -18,3 +19,26 @@ def get_cache_path(self): | |||
|
|
|||
| def get_logs_path(self): | |||
| return Path.home() / f"logs/{App.app.app_id}" | |||
There was a problem hiding this comment.
If we're adding "TOGA_DUMMY_USER_DIRS", we should use it throughout the dummy backend, not just on the new endpoints.
There was a problem hiding this comment.
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.
| # 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. |
There was a problem hiding this comment.
Claude has a habit of being verbose. We don't need that in our codebase.
| # 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. |
There was a problem hiding this comment.
Removed, Also trimmed other comments.
| def _xdg_user_dir(name, fallback): | ||
| """Resolve the location of an XDG user directory. | ||
| Locations follow the freedesktop ``xdg-user-dirs`` tool: the |
There was a problem hiding this comment.
We use Markdown, not ReStructured text.
| Locations follow the freedesktop ``xdg-user-dirs`` tool: the | |
| Locations follow the freedesktop `xdg-user-dirs` tool: the |
|
|
||
|
|
||
| def _xdg_user_dir(name, fallback): | ||
| """Resolve the location of an XDG user directory. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| @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)) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| expected.rmdir() | ||
|
|
||
|
|
||
| async def test_xdg_user_dir_resolution(app_probe, monkeypatch): |
There was a problem hiding this comment.
This should be a parameterised test, not one test generating 4 outcomes.
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
I haven't done a full review yet, but two things stand out:
- There's a lot of merge conflicts that need resolving
- Is there any reason to exclude macOS from the platformdirs-based implementation?
- 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.
|
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>
Adds
app.paths.desktop,app.paths.documents,app.paths.downloadsandapp.paths.pictures, implementing the scope agreed in the discussion on #3551:RuntimeErroris raised if the folder doesn't exist on the device.Environment.GetFolderPath), so folder redirection (e.g., by OneDrive) is honored. The Downloads folder isn't included in .NET'sSpecialFolderenum, so it is obtained from the Win32 known folder API (SHGetKnownFolderPath), following the existing ctypes wrapper style inlibs/.user-dirs.dirsconfiguration, using the same resolution order as platformdirs: the config file (located viaXDG_CONFIG_HOME), then the matchingXDG_*_DIRenvironment variable, then the default English folder name in the user's home folder.RuntimeError, as these platforms don't have user-accessible user-space folders.~/toga-dummy, with aTOGA_DUMMY_USER_DIRSenvironment variable override so test suites stay inside a temporary location.Fixes #3551
Validation
toga/paths.pyat 100% coverage, including branches).PR Checklist:
Assisted-by: Claude Fable 5
🤖 Generated with Claude Code