├── .github ├── CODEOWNERS ├── FUNDING.yml ├── SECURITY.md ├── dependabot.yml ├── release.yml └── workflows │ ├── check.yaml │ └── release.yaml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yml ├── CHANGES.rst ├── CONTRIBUTING.md ├── LICENSE ├── README.rst ├── docs ├── api.rst ├── conf.py └── index.rst ├── pyproject.toml ├── src └── platformdirs │ ├── __init__.py │ ├── __main__.py │ ├── android.py │ ├── api.py │ ├── macos.py │ ├── py.typed │ ├── unix.py │ └── windows.py └── tests ├── conftest.py ├── test_android.py ├── test_api.py ├── test_comp_with_appdirs.py ├── test_macos.py ├── test_main.py └── test_unix.py /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @gaborbernat @ofek @Julian @RonnyPfannschmidt 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | tidelift: "pypi/platformdirs" 2 | -------------------------------------------------------------------------------- /.github/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | ------- | ------------------ | 7 | | 2.5 + | :white_check_mark: | 8 | | < 2.5 | :x: | 9 | 10 | ## Reporting a Vulnerability 11 | 12 | To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift 13 | will coordinate the fix and disclosure. 14 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | groups: 8 | all: 9 | patterns: 10 | - "*" 11 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | authors: 4 | - dependabot 5 | - pre-commit-ci 6 | -------------------------------------------------------------------------------- /.github/workflows/check.yaml: -------------------------------------------------------------------------------- 1 | name: check 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: ["main"] 6 | tags-ignore: ["**"] 7 | pull_request: 8 | schedule: 9 | - cron: "0 8 * * *" 10 | 11 | concurrency: 12 | group: check-${{ github.ref }} 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | test: 17 | name: test ${{ matrix.py }} - ${{ matrix.os }} 18 | runs-on: ${{ matrix.os }} 19 | strategy: 20 | fail-fast: false 21 | matrix: 22 | py: 23 | - "pypy3.10" # ahead to start it earlier because takes longer 24 | - "3.13" 25 | - "3.12" 26 | - "3.11" 27 | - "3.10" 28 | - "3.9" 29 | os: 30 | - ubuntu-latest 31 | - windows-latest 32 | - macos-latest 33 | 34 | steps: 35 | - uses: actions/checkout@v4 36 | with: 37 | fetch-depth: 0 38 | - name: Install the latest version of uv 39 | uses: astral-sh/setup-uv@v6 40 | with: 41 | enable-cache: true 42 | cache-dependency-glob: "pyproject.toml" 43 | github-token: ${{ secrets.GITHUB_TOKEN }} 44 | - name: Add .local/bin to Windows PATH 45 | if: runner.os == 'Windows' 46 | shell: bash 47 | run: echo "$USERPROFILE/.local/bin" >> $GITHUB_PATH 48 | - name: install hatch 49 | run: uv tool install --python-preference only-managed --python 3.13 hatch 50 | - name: install Python 51 | run: uv python install --python-preference only-managed ${{ matrix.py }} 52 | - name: Pick environment to run 53 | run: | 54 | import codecs; import os 55 | py = "${{ matrix.py }}" 56 | py = "test.{}".format(py if py.startswith("pypy") else f"py{py}") 57 | print(f"Picked {py}") 58 | with codecs.open(os.environ["GITHUB_ENV"], mode="a", encoding="utf-8") as file_handler: 59 | file_handler.write(f"FORCE_COLOR=1\nENV={py}\n") 60 | shell: python 61 | - name: Setup test environment 62 | run: | 63 | hatch -v env create ${ENV} 64 | hatch run ${ENV}:uv pip tree 65 | shell: bash 66 | - name: Run test suite 67 | run: hatch -v run ${ENV}:run 68 | env: 69 | PYTEST_ADDOPTS: "-vv --durations=20" 70 | CI_RUN: "yes" 71 | shell: bash 72 | - name: Rename coverage report file 73 | run: | 74 | import os; import sys; 75 | os.rename(f"report{os.sep}.coverage.${{ matrix.py }}", f"report{os.sep}.coverage.${{ matrix.py }}-{sys.platform}") 76 | shell: python 77 | - name: Upload coverage data 78 | uses: actions/upload-artifact@v4 79 | with: 80 | include-hidden-files: true 81 | name: coverage-${{ matrix.os }}-${{ matrix.py }} 82 | path: "report/.coverage.*" 83 | retention-days: 3 84 | 85 | coverage: 86 | name: Combine coverage 87 | runs-on: ubuntu-latest 88 | needs: test 89 | steps: 90 | - name: Let us have colors 91 | run: echo "FORCE_COLOR=true" >> "$GITHUB_ENV" 92 | - uses: actions/checkout@v4 93 | with: 94 | fetch-depth: 0 95 | - name: Install the latest version of uv 96 | uses: astral-sh/setup-uv@v6 97 | with: 98 | enable-cache: true 99 | cache-dependency-glob: "pyproject.toml" 100 | github-token: ${{ secrets.GITHUB_TOKEN }} 101 | - name: install hatch 102 | run: uv tool install --python-preference only-managed --python 3.13 hatch 103 | - name: Setup coverage tool 104 | run: | 105 | hatch -v env create coverage 106 | hatch run coverage:pip freeze 107 | - name: Download coverage data 108 | uses: actions/download-artifact@v4 109 | with: 110 | path: report 111 | pattern: coverage-* 112 | merge-multiple: true 113 | - name: Combine and report coverage 114 | run: hatch run coverage:run 115 | - name: Upload HTML report 116 | uses: actions/upload-artifact@v4 117 | with: 118 | name: html-report 119 | path: report/html 120 | 121 | check: 122 | name: ${{ matrix.env.name }} - ${{ matrix.os }} 123 | runs-on: ${{ matrix.os }} 124 | strategy: 125 | fail-fast: false 126 | matrix: 127 | os: 128 | - ubuntu-latest 129 | - windows-latest 130 | env: 131 | - { "name": "default", "target": "show" } 132 | - { "name": "type", "target": "run" } 133 | - { "name": "docs", "target": "build" } 134 | - { "name": "readme", "target": "run" } 135 | steps: 136 | - uses: actions/checkout@v4 137 | with: 138 | fetch-depth: 0 139 | - name: Install the latest version of uv 140 | uses: astral-sh/setup-uv@v6 141 | with: 142 | enable-cache: true 143 | cache-dependency-glob: "pyproject.toml" 144 | github-token: ${{ secrets.GITHUB_TOKEN }} 145 | - name: Add .local/bin to Windows PATH 146 | if: runner.os == 'Windows' 147 | shell: bash 148 | run: echo "$USERPROFILE/.local/bin" >> $GITHUB_PATH 149 | - name: install hatch 150 | run: uv tool install --python-preference only-managed --python 3.13 hatch 151 | - name: Setup ${{ matrix.env.name }} 152 | run: | 153 | hatch -v env create ${{ matrix.env.name }} 154 | hatch run ${{ matrix.env.name }}:pip freeze 155 | - name: Run ${{ matrix.env.name }} 156 | run: hatch -v run ${{ matrix.env.name }}:${{ matrix.env.target }} 157 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release to PyPI 2 | on: 3 | push: 4 | tags: ["*"] 5 | 6 | env: 7 | dists-artifact-name: python-package-distributions 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | with: 15 | fetch-depth: 0 16 | - name: Install the latest version of uv 17 | uses: astral-sh/setup-uv@v6 18 | with: 19 | enable-cache: true 20 | cache-dependency-glob: "pyproject.toml" 21 | github-token: ${{ secrets.GITHUB_TOKEN }} 22 | - name: Build package 23 | run: uv build --python 3.13 --python-preference only-managed --sdist --wheel . --out-dir dist 24 | - name: Store the distribution packages 25 | uses: actions/upload-artifact@v4 26 | with: 27 | name: ${{ env.dists-artifact-name }} 28 | path: dist/* 29 | 30 | release: 31 | needs: 32 | - build 33 | runs-on: ubuntu-latest 34 | environment: 35 | name: release 36 | url: https://pypi.org/project/platformdirs/${{ github.ref_name }} 37 | permissions: 38 | id-token: write 39 | steps: 40 | - name: Download all the dists 41 | uses: actions/download-artifact@v4 42 | with: 43 | name: ${{ env.dists-artifact-name }} 44 | path: dist/ 45 | - name: Publish to PyPI 46 | uses: pypa/gh-action-pypi-publish@v1.12.4 47 | with: 48 | attestations: true 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.egg-info 3 | /dist 4 | /.tox 5 | /src/platformdirs/version.py 6 | /report 7 | /docs/build 8 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v5.0.0 4 | hooks: 5 | - id: end-of-file-fixer 6 | - id: trailing-whitespace 7 | - repo: https://github.com/python-jsonschema/check-jsonschema 8 | rev: 0.33.0 9 | hooks: 10 | - id: check-github-workflows 11 | args: ["--verbose"] 12 | - repo: https://github.com/codespell-project/codespell 13 | rev: v2.4.1 14 | hooks: 15 | - id: codespell 16 | additional_dependencies: ["tomli>=2.2.1"] 17 | - repo: https://github.com/tox-dev/pyproject-fmt 18 | rev: "v2.6.0" 19 | hooks: 20 | - id: pyproject-fmt 21 | - repo: https://github.com/astral-sh/ruff-pre-commit 22 | rev: "v0.11.11" 23 | hooks: 24 | - id: ruff-format 25 | - id: ruff 26 | args: ["--fix", "--unsafe-fixes", "--exit-non-zero-on-fix"] 27 | - repo: https://github.com/rbubley/mirrors-prettier 28 | rev: "v3.5.3" 29 | hooks: 30 | - id: prettier 31 | args: ["--print-width=120", "--prose-wrap=always"] 32 | - repo: meta 33 | hooks: 34 | - id: check-hooks-apply 35 | - id: check-useless-excludes 36 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | build: 3 | os: ubuntu-22.04 4 | tools: 5 | python: "3" 6 | python: 7 | install: 8 | - method: pip 9 | path: . 10 | extra_requirements: 11 | - docs 12 | sphinx: 13 | builder: html 14 | configuration: docs/conf.py 15 | fail_on_warning: true 16 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | platformdirs Changelog 2 | ====================== 3 | 4 | The changes of more recent versions are `tracked here `_. 5 | 6 | platformdirs 4.0.0 (2023-11-10) 7 | ------------------------------- 8 | - UNIX: revert site_cache_dir to use ``/var/cache`` instead of ``/var/tmp`` 9 | 10 | platformdirs 3.8.1 (2023-07-06) 11 | ------------------------------- 12 | - BSD: provide a fallback for ``user_runtime_dir`` 13 | 14 | platformdirs 3.8.0 (2023-06-22) 15 | ------------------------------- 16 | - Add missing user media directory docs 17 | 18 | platformdirs 3.7.0 (2023-06-20) 19 | ------------------------------- 20 | - Have user_runtime_dir return /var/run/user/uid for \*BSD 21 | 22 | platformdirs 3.6.0 (2023-06-19) 23 | ------------------------------- 24 | - introduce ``user_downloads_dir`` 25 | 26 | platformdirs 3.5.3 (2023-06-09) 27 | ------------------------------- 28 | - Use ruff 29 | 30 | platformdirs 3.5.2 (2023-05-30) 31 | ------------------------------- 32 | - test with 3.12.0.b1 33 | 34 | platformdirs 3.5.1 (2023-05-11) 35 | ------------------------------- 36 | - Add 3.12 support 37 | - Add tox.ini to sdist 38 | - removing Windows versions 39 | - Better handling for UNIX support 40 | 41 | platformdirs 3.5.0 (2023-04-27) 42 | ------------------------------- 43 | - introduce ``user_music_dir`` 44 | 45 | platformdirs 3.4.0 (2023-04-26) 46 | ------------------------------- 47 | - introduce ``user_videos_dir`` 48 | 49 | platformdirs 3.3.0 (2023-04-25) 50 | ------------------------------- 51 | - introduce ``user_pictures_dir`` 52 | 53 | platformdirs 3.2.0 (2023-03-25) 54 | ------------------------------- 55 | - add auto create directories optional 56 | 57 | platformdirs 3.1.1 (2023-03-10) 58 | ------------------------------- 59 | - site_cache_dir use ``/var/tmp`` instead of ``/var/cache`` on unix, as the later may be write protected 60 | 61 | platformdirs 3.1.0 (2023-03-03) 62 | ------------------------------- 63 | - Introduce ``site_cache_dir`` 64 | 65 | platformdirs 3.0.1 (2023-03-02) 66 | ------------------------------- 67 | - Add ``appdirs`` keyword to package 68 | 69 | platformdirs 3.0.0 (2023-02-06) 70 | ------------------------------- 71 | - **BREAKING** Changed the config directory on macOS to point to ``*/Library/Application Support`` 72 | - macOS: remove erroneous trailing slash from ``user_config_dir`` and ``user_data_dir`` 73 | 74 | platformdirs 2.6.2 (2022-12-28) 75 | ------------------------------- 76 | - Fix missing ``typing-extensions`` dependency. 77 | 78 | platformdirs 2.6.1 (2022-12-28) 79 | ------------------------------- 80 | - Add detection of ``$PREFIX`` for android. 81 | 82 | platformdirs 2.6.0 (2022-12-06) 83 | ------------------------------- 84 | - **BREAKING** Correct the log directory on Linux/Unix from ``XDG_CACHE_HOME`` to ``XDG_STATE_HOME`` per the XDG spec 85 | 86 | platformdirs 2.5.4 (2022-11-12) 87 | ------------------------------- 88 | - Fix licensing metadata 89 | 90 | platformdirs 2.5.3 (2022-11-06) 91 | ------------------------------- 92 | - Support 3.11 93 | - Bump dependencies 94 | 95 | platformdirs 2.5.2 (2022-04-18) 96 | ------------------------------- 97 | - Move packaging to hatchling from setuptools 98 | - Treat android shells as unix 99 | 100 | platformdirs 2.5.1 (2022-02-19) 101 | ------------------------------- 102 | - Add native support for nuitka 103 | 104 | platformdirs 2.5.0 (2022-02-09) 105 | ------------------------------- 106 | - Add support for Termux subsystems 107 | 108 | platformdirs 2.4.1 (2021-12-26) 109 | ------------------------------- 110 | - Drop python 3.6 support 111 | 112 | platformdirs 2.4.0 (2021-09-25) 113 | ------------------------------- 114 | - Add ``user_documents_dir`` 115 | 116 | platformdirs 2.3.0 (2021-08-31) 117 | ------------------------------- 118 | - Add ``user_runtime_dir`` and its path-returning equivalent (#37) 119 | 120 | platformdirs 2.2.0 (2021-07-29) 121 | ------------------------------- 122 | - Unix: Fallback to default if XDG environment variable is empty 123 | 124 | platformdirs 2.1.0 (2021-07-25) 125 | ------------------------------- 126 | - Add ``readthedocs.org`` documentation via Sphinx 127 | - Modernize project layout 128 | - Drop Python 2.7 and 3.5 support 129 | - Android support 130 | - Add type annotations 131 | - Reorganize project layout to platform specific classes, see 132 | :class:`PlatformDirsABC ` and it's implementations: 133 | :class:`Android `, :class:`MacOS `, 134 | :class:`Unix ` and :class:`Windows ` 135 | - Add ``*_path`` API, returning :class:`pathlib.Path ` objects instead of :class:`str` 136 | (``user_data_path``, ``user_config_path``, ``user_cache_path``, ``user_state_path``, ``user_log_path``, 137 | ``site_data_path``, ``site_config_path``) - by `@papr `_ 138 | 139 | platformdirs 2.0.2 (2021-07-13) 140 | ------------------------------- 141 | - Fix ``__version__`` and ``__version_info__`` 142 | 143 | platformdirs 2.0.1 (never released) 144 | ----------------------------------- 145 | - Documentation fixes 146 | 147 | platformdirs 2.0.0 (2021-07-12) 148 | ------------------------------- 149 | 150 | - **BREAKING** Name change as part of the friendly fork 151 | - **BREAKING** Remove support for end-of-life Pythons 2.6, 3.2, and 3.3 152 | - **BREAKING** Correct the config directory on OSX/macOS 153 | - Add Python 3.7, 3.8, and 3.9 support 154 | 155 | appdirs 1.4.4 (2020-05-11) 156 | -------------------------- 157 | - [PR #92] Don't import appdirs from setup.py which resolves issue #91 158 | 159 | Project officially classified as Stable which is important 160 | for inclusion in other distros such as ActivePython. 161 | 162 | appdirs 1.4.3 (2017-03-07) 163 | -------------------------- 164 | - [PR #76] Python 3.6 invalid escape sequence deprecation fixes 165 | - Fix for Python 3.6 support 166 | 167 | appdirs 1.4.2 (2017-02-24) 168 | -------------------------- 169 | - [PR #84] Allow installing without setuptools 170 | - [PR #86] Fix string delimiters in setup.py description 171 | - Add Python 3.6 support 172 | 173 | appdirs 1.4.1 (2017-02-23) 174 | -------------------------- 175 | - [issue #38] Fix _winreg import on Windows Py3 176 | - [issue #55] Make appname optional 177 | 178 | appdirs 1.4.0 (2017-08-17) 179 | -------------------------- 180 | - [PR #42] AppAuthor is now optional on Windows 181 | - [issue 41] Support Jython on Windows, Mac, and Unix-like platforms. Windows 182 | support requires `JNA `_. 183 | - [PR #44] Fix incorrect behavior of the site_config_dir method 184 | 185 | appdirs 1.3.0 (2014-04-22) 186 | -------------------------- 187 | - [Unix, issue 16] Conform to XDG standard, instead of breaking it for 188 | everybody 189 | - [Unix] Removes gratuitous case mangling of the case, since \*nix-es are 190 | usually case sensitive, so mangling is not wise 191 | - [Unix] Fixes the utterly wrong behavior in ``site_data_dir``, return result 192 | based on XDG_DATA_DIRS and make room for respecting the standard which 193 | specifies XDG_DATA_DIRS is a multiple-value variable 194 | - [Issue 6] Add ``*_config_dir`` which are distinct on nix-es, according to 195 | XDG specs; on Windows and Mac return the corresponding ``*_data_dir`` 196 | 197 | appdirs 1.2.0 (2011-01-26) 198 | -------------------------- 199 | 200 | - [Unix] Put ``user_log_dir`` under the *cache* dir on Unix. Seems to be more 201 | typical. 202 | - [issue 9] Make ``unicode`` work on py3k. 203 | 204 | appdirs 1.1.0 (2010-09-02) 205 | -------------------------- 206 | 207 | - [issue 4] Add ``AppDirs.user_log_dir``. 208 | - [Unix, issue 2, issue 7] appdirs now conforms to `XDG base directory spec 209 | `_. 210 | - [Mac, issue 5] Fix ``site_data_dir()`` on Mac. 211 | - [Mac] Drop use of 'Carbon' module in favor of hardcoded paths; supports 212 | Python3 now. 213 | - [Windows] Append "Cache" to ``user_cache_dir`` on Windows by default. Use 214 | ``opinion=False`` option to disable this. 215 | - Add ``appdirs.AppDirs`` convenience class. Usage: 216 | 217 | >>> dirs = AppDirs("SuperApp", "Acme", version="1.0") 218 | >>> dirs.user_data_dir 219 | '/Users/trentm/Library/Application Support/SuperApp/1.0' 220 | 221 | - [Windows] Cherry-pick Komodo's change to downgrade paths to the Windows short 222 | paths if there are high bit chars. 223 | - [Linux] Change default ``user_cache_dir()`` on Linux to be singular, e.g. 224 | "~/.superapp/cache". 225 | - [Windows] Add ``roaming`` option to ``user_data_dir()`` (for use on Windows only) 226 | and change the default ``user_data_dir`` behavior to use a *non*-roaming 227 | profile dir (``CSIDL_LOCAL_APPDATA`` instead of ``CSIDL_APPDATA``). Why? Because 228 | a large roaming profile can cause login speed issues. The "only syncs on 229 | logout" behavior can cause surprises in appdata info. 230 | 231 | 232 | appdirs 1.0.1 (never released) 233 | ------------------------------ 234 | 235 | Started this changelog 27 July 2010. Before that this module originated in the 236 | `Komodo `_ product as ``applib.py`` and then 237 | as ``applib/location.py`` (used by `PyPM `_ in `ActivePython 238 | `_). This is basically a fork of applib.py 1.0.1 and applib/location.py 1.0.1. 239 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | This project uses [hatch](https://hatch.pypa.io) development, therefore consult that documentation for more in-depth how 2 | to. To see a list of available environments use `hatch env show`, for example to run the test suite under Python 3.12 3 | can type in a shell `hatch run test.py3.12:run`. 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2010-202x The platformdirs developers 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | The problem 2 | =========== 3 | 4 | .. image:: https://badge.fury.io/py/platformdirs.svg 5 | :target: https://badge.fury.io/py/platformdirs 6 | .. image:: https://img.shields.io/pypi/pyversions/platformdirs.svg 7 | :target: https://pypi.python.org/pypi/platformdirs/ 8 | .. image:: https://github.com/tox-dev/platformdirs/actions/workflows/check.yaml/badge.svg 9 | :target: https://github.com/platformdirs/platformdirs/actions 10 | .. image:: https://static.pepy.tech/badge/platformdirs/month 11 | :target: https://pepy.tech/project/platformdirs 12 | 13 | When writing desktop application, finding the right location to store user data 14 | and configuration varies per platform. Even for single-platform apps, there 15 | may by plenty of nuances in figuring out the right location. 16 | 17 | For example, if running on macOS, you should use:: 18 | 19 | ~/Library/Application Support/ 20 | 21 | If on Windows (at least English Win) that should be:: 22 | 23 | C:\Documents and Settings\\Application Data\Local Settings\\ 24 | 25 | or possibly:: 26 | 27 | C:\Documents and Settings\\Application Data\\ 28 | 29 | for `roaming profiles `_ but that is another story. 30 | 31 | On Linux (and other Unices), according to the `XDG Basedir Spec`_, it should be:: 32 | 33 | ~/.local/share/ 34 | 35 | .. _XDG Basedir Spec: https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html 36 | 37 | ``platformdirs`` to the rescue 38 | ============================== 39 | 40 | This kind of thing is what the ``platformdirs`` package is for. 41 | ``platformdirs`` will help you choose an appropriate: 42 | 43 | - user data dir (``user_data_dir``) 44 | - user config dir (``user_config_dir``) 45 | - user cache dir (``user_cache_dir``) 46 | - site data dir (``site_data_dir``) 47 | - site config dir (``site_config_dir``) 48 | - user log dir (``user_log_dir``) 49 | - user documents dir (``user_documents_dir``) 50 | - user downloads dir (``user_downloads_dir``) 51 | - user pictures dir (``user_pictures_dir``) 52 | - user videos dir (``user_videos_dir``) 53 | - user music dir (``user_music_dir``) 54 | - user desktop dir (``user_desktop_dir``) 55 | - user runtime dir (``user_runtime_dir``) 56 | 57 | And also: 58 | 59 | - Is slightly opinionated on the directory names used. Look for "OPINION" in 60 | documentation and code for when an opinion is being applied. 61 | 62 | Example output 63 | ============== 64 | 65 | On macOS: 66 | 67 | .. code-block:: pycon 68 | 69 | >>> from platformdirs import * 70 | >>> appname = "SuperApp" 71 | >>> appauthor = "Acme" 72 | >>> user_data_dir(appname, appauthor) 73 | '/Users/trentm/Library/Application Support/SuperApp' 74 | >>> user_config_dir(appname, appauthor) 75 | '/Users/trentm/Library/Application Support/SuperApp' 76 | >>> user_cache_dir(appname, appauthor) 77 | '/Users/trentm/Library/Caches/SuperApp' 78 | >>> site_data_dir(appname, appauthor) 79 | '/Library/Application Support/SuperApp' 80 | >>> site_config_dir(appname, appauthor) 81 | '/Library/Application Support/SuperApp' 82 | >>> user_log_dir(appname, appauthor) 83 | '/Users/trentm/Library/Logs/SuperApp' 84 | >>> user_documents_dir() 85 | '/Users/trentm/Documents' 86 | >>> user_downloads_dir() 87 | '/Users/trentm/Downloads' 88 | >>> user_pictures_dir() 89 | '/Users/trentm/Pictures' 90 | >>> user_videos_dir() 91 | '/Users/trentm/Movies' 92 | >>> user_music_dir() 93 | '/Users/trentm/Music' 94 | >>> user_desktop_dir() 95 | '/Users/trentm/Desktop' 96 | >>> user_runtime_dir(appname, appauthor) 97 | '/Users/trentm/Library/Caches/TemporaryItems/SuperApp' 98 | 99 | On Windows: 100 | 101 | .. code-block:: pycon 102 | 103 | >>> from platformdirs import * 104 | >>> appname = "SuperApp" 105 | >>> appauthor = "Acme" 106 | >>> user_data_dir(appname, appauthor) 107 | 'C:\\Users\\trentm\\AppData\\Local\\Acme\\SuperApp' 108 | >>> user_data_dir(appname, appauthor, roaming=True) 109 | 'C:\\Users\\trentm\\AppData\\Roaming\\Acme\\SuperApp' 110 | >>> user_config_dir(appname, appauthor) 111 | 'C:\\Users\\trentm\\AppData\\Local\\Acme\\SuperApp' 112 | >>> user_cache_dir(appname, appauthor) 113 | 'C:\\Users\\trentm\\AppData\\Local\\Acme\\SuperApp\\Cache' 114 | >>> site_data_dir(appname, appauthor) 115 | 'C:\\ProgramData\\Acme\\SuperApp' 116 | >>> site_config_dir(appname, appauthor) 117 | 'C:\\ProgramData\\Acme\\SuperApp' 118 | >>> user_log_dir(appname, appauthor) 119 | 'C:\\Users\\trentm\\AppData\\Local\\Acme\\SuperApp\\Logs' 120 | >>> user_documents_dir() 121 | 'C:\\Users\\trentm\\Documents' 122 | >>> user_downloads_dir() 123 | 'C:\\Users\\trentm\\Downloads' 124 | >>> user_pictures_dir() 125 | 'C:\\Users\\trentm\\Pictures' 126 | >>> user_videos_dir() 127 | 'C:\\Users\\trentm\\Videos' 128 | >>> user_music_dir() 129 | 'C:\\Users\\trentm\\Music' 130 | >>> user_desktop_dir() 131 | 'C:\\Users\\trentm\\Desktop' 132 | >>> user_runtime_dir(appname, appauthor) 133 | 'C:\\Users\\trentm\\AppData\\Local\\Temp\\Acme\\SuperApp' 134 | 135 | On Linux: 136 | 137 | .. code-block:: pycon 138 | 139 | >>> from platformdirs import * 140 | >>> appname = "SuperApp" 141 | >>> appauthor = "Acme" 142 | >>> user_data_dir(appname, appauthor) 143 | '/home/trentm/.local/share/SuperApp' 144 | >>> user_config_dir(appname) 145 | '/home/trentm/.config/SuperApp' 146 | >>> user_cache_dir(appname, appauthor) 147 | '/home/trentm/.cache/SuperApp' 148 | >>> site_data_dir(appname, appauthor) 149 | '/usr/local/share/SuperApp' 150 | >>> site_data_dir(appname, appauthor, multipath=True) 151 | '/usr/local/share/SuperApp:/usr/share/SuperApp' 152 | >>> site_config_dir(appname) 153 | '/etc/xdg/SuperApp' 154 | >>> os.environ["XDG_CONFIG_DIRS"] = "/etc:/usr/local/etc" 155 | >>> site_config_dir(appname, multipath=True) 156 | '/etc/SuperApp:/usr/local/etc/SuperApp' 157 | >>> user_log_dir(appname, appauthor) 158 | '/home/trentm/.local/state/SuperApp/log' 159 | >>> user_documents_dir() 160 | '/home/trentm/Documents' 161 | >>> user_downloads_dir() 162 | '/home/trentm/Downloads' 163 | >>> user_pictures_dir() 164 | '/home/trentm/Pictures' 165 | >>> user_videos_dir() 166 | '/home/trentm/Videos' 167 | >>> user_music_dir() 168 | '/home/trentm/Music' 169 | >>> user_desktop_dir() 170 | '/home/trentm/Desktop' 171 | >>> user_runtime_dir(appname, appauthor) 172 | '/run/user/{os.getuid()}/SuperApp' 173 | 174 | On Android:: 175 | 176 | >>> from platformdirs import * 177 | >>> appname = "SuperApp" 178 | >>> appauthor = "Acme" 179 | >>> user_data_dir(appname, appauthor) 180 | '/data/data/com.myApp/files/SuperApp' 181 | >>> user_config_dir(appname) 182 | '/data/data/com.myApp/shared_prefs/SuperApp' 183 | >>> user_cache_dir(appname, appauthor) 184 | '/data/data/com.myApp/cache/SuperApp' 185 | >>> site_data_dir(appname, appauthor) 186 | '/data/data/com.myApp/files/SuperApp' 187 | >>> site_config_dir(appname) 188 | '/data/data/com.myApp/shared_prefs/SuperApp' 189 | >>> user_log_dir(appname, appauthor) 190 | '/data/data/com.myApp/cache/SuperApp/log' 191 | >>> user_documents_dir() 192 | '/storage/emulated/0/Documents' 193 | >>> user_downloads_dir() 194 | '/storage/emulated/0/Downloads' 195 | >>> user_pictures_dir() 196 | '/storage/emulated/0/Pictures' 197 | >>> user_videos_dir() 198 | '/storage/emulated/0/DCIM/Camera' 199 | >>> user_music_dir() 200 | '/storage/emulated/0/Music' 201 | >>> user_desktop_dir() 202 | '/storage/emulated/0/Desktop' 203 | >>> user_runtime_dir(appname, appauthor) 204 | '/data/data/com.myApp/cache/SuperApp/tmp' 205 | 206 | Note: Some android apps like Termux and Pydroid are used as shells. These 207 | apps are used by the end user to emulate Linux environment. Presence of 208 | ``SHELL`` environment variable is used by Platformdirs to differentiate 209 | between general android apps and android apps used as shells. Shell android 210 | apps also support ``XDG_*`` environment variables. 211 | 212 | 213 | ``PlatformDirs`` for convenience 214 | ================================ 215 | 216 | .. code-block:: pycon 217 | 218 | >>> from platformdirs import PlatformDirs 219 | >>> dirs = PlatformDirs("SuperApp", "Acme") 220 | >>> dirs.user_data_dir 221 | '/Users/trentm/Library/Application Support/SuperApp' 222 | >>> dirs.user_config_dir 223 | '/Users/trentm/Library/Application Support/SuperApp' 224 | >>> dirs.user_cache_dir 225 | '/Users/trentm/Library/Caches/SuperApp' 226 | >>> dirs.site_data_dir 227 | '/Library/Application Support/SuperApp' 228 | >>> dirs.site_config_dir 229 | '/Library/Application Support/SuperApp' 230 | >>> dirs.user_cache_dir 231 | '/Users/trentm/Library/Caches/SuperApp' 232 | >>> dirs.user_log_dir 233 | '/Users/trentm/Library/Logs/SuperApp' 234 | >>> dirs.user_documents_dir 235 | '/Users/trentm/Documents' 236 | >>> dirs.user_downloads_dir 237 | '/Users/trentm/Downloads' 238 | >>> dirs.user_pictures_dir 239 | '/Users/trentm/Pictures' 240 | >>> dirs.user_videos_dir 241 | '/Users/trentm/Movies' 242 | >>> dirs.user_music_dir 243 | '/Users/trentm/Music' 244 | >>> dirs.user_desktop_dir 245 | '/Users/trentm/Desktop' 246 | >>> dirs.user_runtime_dir 247 | '/Users/trentm/Library/Caches/TemporaryItems/SuperApp' 248 | 249 | Per-version isolation 250 | ===================== 251 | 252 | If you have multiple versions of your app in use that you want to be 253 | able to run side-by-side, then you may want version-isolation for these 254 | dirs:: 255 | 256 | >>> from platformdirs import PlatformDirs 257 | >>> dirs = PlatformDirs("SuperApp", "Acme", version="1.0") 258 | >>> dirs.user_data_dir 259 | '/Users/trentm/Library/Application Support/SuperApp/1.0' 260 | >>> dirs.user_config_dir 261 | '/Users/trentm/Library/Application Support/SuperApp/1.0' 262 | >>> dirs.user_cache_dir 263 | '/Users/trentm/Library/Caches/SuperApp/1.0' 264 | >>> dirs.site_data_dir 265 | '/Library/Application Support/SuperApp/1.0' 266 | >>> dirs.site_config_dir 267 | '/Library/Application Support/SuperApp/1.0' 268 | >>> dirs.user_log_dir 269 | '/Users/trentm/Library/Logs/SuperApp/1.0' 270 | >>> dirs.user_documents_dir 271 | '/Users/trentm/Documents' 272 | >>> dirs.user_downloads_dir 273 | '/Users/trentm/Downloads' 274 | >>> dirs.user_pictures_dir 275 | '/Users/trentm/Pictures' 276 | >>> dirs.user_videos_dir 277 | '/Users/trentm/Movies' 278 | >>> dirs.user_music_dir 279 | '/Users/trentm/Music' 280 | >>> dirs.user_desktop_dir 281 | '/Users/trentm/Desktop' 282 | >>> dirs.user_runtime_dir 283 | '/Users/trentm/Library/Caches/TemporaryItems/SuperApp/1.0' 284 | 285 | Be wary of using this for configuration files though; you'll need to handle 286 | migrating configuration files manually. 287 | 288 | Why this Fork? 289 | ============== 290 | 291 | This repository is a friendly fork of the wonderful work started by 292 | `ActiveState `_ who created 293 | ``appdirs``, this package's ancestor. 294 | 295 | Maintaining an open source project is no easy task, particularly 296 | from within an organization, and the Python community is indebted 297 | to ``appdirs`` (and to Trent Mick and Jeff Rouse in particular) for 298 | creating an incredibly useful simple module, as evidenced by the wide 299 | number of users it has attracted over the years. 300 | 301 | Nonetheless, given the number of long-standing open issues 302 | and pull requests, and no clear path towards `ensuring 303 | that maintenance of the package would continue or grow 304 | `_, this fork was 305 | created. 306 | 307 | Contributions are most welcome. 308 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | API 2 | === 3 | 4 | User directories 5 | ~~~~~~~~~~~~~~~~ 6 | 7 | These are user-specific (and, generally, user-writeable) directories. 8 | 9 | User data directory 10 | ------------------- 11 | 12 | .. autofunction:: platformdirs.user_data_dir 13 | .. autofunction:: platformdirs.user_data_path 14 | 15 | User config directory 16 | --------------------- 17 | 18 | .. autofunction:: platformdirs.user_config_dir 19 | .. autofunction:: platformdirs.user_config_path 20 | 21 | Cache directory 22 | ------------------- 23 | 24 | .. autofunction:: platformdirs.user_cache_dir 25 | .. autofunction:: platformdirs.user_cache_path 26 | 27 | State directory 28 | ------------------- 29 | 30 | .. autofunction:: platformdirs.user_state_dir 31 | .. autofunction:: platformdirs.user_state_path 32 | 33 | Logs directory 34 | ------------------- 35 | 36 | .. autofunction:: platformdirs.user_log_dir 37 | .. autofunction:: platformdirs.user_log_path 38 | 39 | User documents directory 40 | ------------------------ 41 | 42 | .. autofunction:: platformdirs.user_documents_dir 43 | .. autofunction:: platformdirs.user_documents_path 44 | 45 | User downloads directory 46 | ------------------------ 47 | 48 | .. autofunction:: platformdirs.user_downloads_dir 49 | .. autofunction:: platformdirs.user_downloads_path 50 | 51 | User pictures directory 52 | ------------------------ 53 | 54 | .. autofunction:: platformdirs.user_pictures_dir 55 | .. autofunction:: platformdirs.user_pictures_path 56 | 57 | User videos directory 58 | ------------------------ 59 | 60 | .. autofunction:: platformdirs.user_videos_dir 61 | .. autofunction:: platformdirs.user_videos_path 62 | 63 | User music directory 64 | ------------------------ 65 | 66 | .. autofunction:: platformdirs.user_music_dir 67 | .. autofunction:: platformdirs.user_music_path 68 | 69 | Runtime directory 70 | ------------------- 71 | 72 | .. autofunction:: platformdirs.user_runtime_dir 73 | .. autofunction:: platformdirs.user_runtime_path 74 | 75 | Shared directories 76 | ~~~~~~~~~~~~~~~~~~ 77 | 78 | These are system-wide (and, generally, read-only) directories. 79 | 80 | Shared data directory 81 | --------------------- 82 | 83 | .. autofunction:: platformdirs.site_data_dir 84 | .. autofunction:: platformdirs.site_data_path 85 | 86 | Shared config directory 87 | ----------------------- 88 | 89 | .. autofunction:: platformdirs.site_config_dir 90 | .. autofunction:: platformdirs.site_config_path 91 | 92 | Shared cache directory 93 | ---------------------- 94 | 95 | .. autofunction:: platformdirs.site_cache_dir 96 | .. autofunction:: platformdirs.site_cache_path 97 | 98 | Platforms 99 | ~~~~~~~~~ 100 | 101 | ABC 102 | --- 103 | .. autoclass:: platformdirs.api.PlatformDirsABC 104 | :members: 105 | :special-members: __init__ 106 | 107 | PlatformDirs 108 | ------------ 109 | 110 | .. autoclass:: platformdirs.PlatformDirs 111 | :members: 112 | :show-inheritance: 113 | 114 | Android 115 | ------- 116 | .. autoclass:: platformdirs.android.Android 117 | :members: 118 | :show-inheritance: 119 | 120 | macOS 121 | ----- 122 | .. autoclass:: platformdirs.macos.MacOS 123 | :members: 124 | :show-inheritance: 125 | 126 | Unix (Linux) 127 | ------------ 128 | .. autoclass:: platformdirs.unix.Unix 129 | :members: 130 | :show-inheritance: 131 | 132 | Windows 133 | ------- 134 | .. autoclass:: platformdirs.windows.Windows 135 | :members: 136 | :show-inheritance: 137 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # noqa: INP001 2 | """Configuration for Sphinx.""" 3 | 4 | from __future__ import annotations 5 | 6 | from datetime import datetime, timezone 7 | 8 | from platformdirs.version import __version__ 9 | 10 | author = "The platformdirs team" 11 | project = "platformdirs" 12 | copyright = "2021, The platformdirs team" # noqa: A001 13 | 14 | release = __version__ 15 | version = release 16 | extensions = [ 17 | "sphinx.ext.autodoc", 18 | "sphinx.ext.autosectionlabel", 19 | "sphinx.ext.viewcode", 20 | "sphinx.ext.intersphinx", 21 | "sphinx_autodoc_typehints", 22 | ] 23 | html_theme = "furo" 24 | html_title, html_last_updated_fmt = "platformdirs", datetime.now(tz=timezone.utc).isoformat() 25 | pygments_style, pygments_dark_style = "sphinx", "monokai" 26 | autoclass_content, autodoc_member_order, autodoc_typehints = "class", "bysource", "none" 27 | autodoc_default_options = { 28 | "member-order": "bysource", 29 | "undoc-members": True, 30 | "show-inheritance": True, 31 | } 32 | default_role = "any" 33 | autosectionlabel_prefix_document = True 34 | intersphinx_mapping = {"python": ("https://docs.python.org/3", None)} 35 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | platformdirs's documentation 2 | ============================ 3 | 4 | ``platformdirs`` is a library to determine platform-specific system directories. 5 | This includes directories where to place cache files, user data, configuration, 6 | etc. 7 | 8 | The source code and issue tracker are both hosted on `GitHub`_. 9 | 10 | .. _GitHub: https://github.com/platformdirs/platformdirs 11 | 12 | .. toctree:: 13 | :maxdepth: 3 14 | 15 | api 16 | 17 | Indices and tables 18 | ================== 19 | 20 | * :ref:`genindex` 21 | * :ref:`modindex` 22 | * :ref:`search` 23 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = "hatchling.build" 3 | requires = [ 4 | "hatch-vcs>=0.4", 5 | "hatchling>=1.27", 6 | ] 7 | 8 | [project] 9 | name = "platformdirs" 10 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." 11 | readme = "README.rst" 12 | keywords = [ 13 | "appdirs", 14 | "application", 15 | "cache", 16 | "directory", 17 | "log", 18 | "user", 19 | ] 20 | license = "MIT" 21 | maintainers = [ 22 | { name = "Bernát Gábor", email = "gaborjbernat@gmail.com" }, 23 | { name = "Julian Berman", email = "Julian@GrayVines.com" }, 24 | { name = "Ofek Lev", email = "oss@ofek.dev" }, 25 | { name = "Ronny Pfannschmidt", email = "opensource@ronnypfannschmidt.de" }, 26 | ] 27 | requires-python = ">=3.9" 28 | classifiers = [ 29 | "Development Status :: 5 - Production/Stable", 30 | "Intended Audience :: Developers", 31 | "License :: OSI Approved :: MIT License", 32 | "Operating System :: OS Independent", 33 | "Programming Language :: Python", 34 | "Programming Language :: Python :: 3 :: Only", 35 | "Programming Language :: Python :: 3.9", 36 | "Programming Language :: Python :: 3.10", 37 | "Programming Language :: Python :: 3.11", 38 | "Programming Language :: Python :: 3.12", 39 | "Programming Language :: Python :: 3.13", 40 | "Programming Language :: Python :: Implementation :: CPython", 41 | "Programming Language :: Python :: Implementation :: PyPy", 42 | "Topic :: Software Development :: Libraries :: Python Modules", 43 | ] 44 | dynamic = [ 45 | "version", 46 | ] 47 | optional-dependencies.docs = [ 48 | "furo>=2024.8.6", 49 | "proselint>=0.14", 50 | "sphinx>=8.1.3", 51 | "sphinx-autodoc-typehints>=3", 52 | ] 53 | optional-dependencies.test = [ 54 | "appdirs==1.4.4", 55 | "covdefaults>=2.3", 56 | "pytest>=8.3.4", 57 | "pytest-cov>=6", 58 | "pytest-mock>=3.14", 59 | ] 60 | optional-dependencies.type = [ 61 | "mypy>=1.14.1", 62 | ] 63 | 64 | urls.Changelog = "https://github.com/tox-dev/platformdirs/releases" 65 | urls.Documentation = "https://platformdirs.readthedocs.io" 66 | urls.Homepage = "https://github.com/tox-dev/platformdirs" 67 | urls.Source = "https://github.com/tox-dev/platformdirs" 68 | urls.Tracker = "https://github.com/tox-dev/platformdirs/issues" 69 | 70 | [tool.hatch] 71 | build.hooks.vcs.version-file = "src/platformdirs/version.py" 72 | build.targets.sdist.include = [ 73 | "/src", 74 | "/tests", 75 | "/tox.ini", 76 | ] 77 | version.source = "vcs" 78 | 79 | [tool.hatch.envs.default] 80 | installer = "uv" 81 | description = "Development environment" 82 | features = [ 83 | "test", 84 | "docs", 85 | "type", 86 | ] 87 | scripts = { show = [ 88 | "uv pip tree", 89 | 'python -c "import sys; print(sys.executable)"', 90 | ] } 91 | 92 | [tool.hatch.envs.test] 93 | template = "test" 94 | installer = "uv" 95 | # dev-mode = false # cannot enable this until https://github.com/pypa/hatch/issues/1237 96 | description = "Run the test suite" 97 | matrix = [ 98 | { python = [ 99 | "3.13", 100 | "3.12", 101 | "3.11", 102 | "3.10", 103 | "3.9", 104 | "pypy3.10", 105 | ] }, 106 | ] 107 | features = [ 108 | "test", 109 | ] 110 | env-vars.COVERAGE_FILE = "report/.coverage.{matrix:python}" 111 | env-vars.COVERAGE_PROCESS_START = "pyproject.toml" 112 | env-vars._COVERAGE_SRC = "src/platformdirs" 113 | [tool.hatch.envs.test.scripts] 114 | run = [ 115 | """ 116 | pytest --junitxml report/junit.{matrix:python}.xml --cov src/platformdirs --cov tests \ 117 | --cov-config=pyproject.toml --no-cov-on-fail --cov-report term-missing:skip-covered --cov-context=test \ 118 | --cov-report html:report/html{matrix:python} --cov-report xml:report/coverage{matrix:python}.xml \ 119 | tests 120 | """, 121 | ] 122 | 123 | [tool.hatch.envs.coverage] 124 | template = "coverage" 125 | installer = "uv" 126 | description = "combine coverage files and generate diff" 127 | dependencies = [ 128 | "covdefaults>=2.3", 129 | "coverage[toml]>=7.6.1", 130 | "diff-cover>=9.2", 131 | ] 132 | env-vars = { COVERAGE_FILE = "report/.coverage" } 133 | [tool.hatch.envs.coverage.scripts] 134 | run = [ 135 | "coverage combine report", 136 | "coverage report --skip-covered --show-missing", 137 | "coverage xml -o report/coverage.xml", 138 | "coverage html -d report/html", 139 | "diff-cover --compare-branch {env:DIFF_AGAINST:origin/main} report/coverage.xml", 140 | ] 141 | 142 | [tool.hatch.envs.type] 143 | template = "type" 144 | installer = "uv" 145 | description = "Run the type checker" 146 | python = "3.12" 147 | dev-mode = false 148 | features = [ 149 | "type", 150 | "test", 151 | ] 152 | scripts = { run = [ 153 | "mypy --strict src", 154 | "mypy --strict tests", 155 | ] } 156 | 157 | [tool.hatch.envs.fix] 158 | template = "fix" 159 | installer = "uv" 160 | description = "Run the pre-commit tool to lint and autofix issues" 161 | python = "3.12" 162 | detached = true 163 | dependencies = [ 164 | "pre-commit-uv>=4.1", 165 | ] 166 | scripts = { "run" = [ 167 | "pre-commit run --all-files --show-diff-on-failure", 168 | ] } 169 | 170 | [tool.hatch.envs.docs] 171 | template = "docs" 172 | installer = "uv" 173 | description = "Build documentation using Sphinx" 174 | python = "3.12" 175 | dev-mode = false 176 | features = [ 177 | "docs", 178 | ] 179 | [tool.hatch.envs.docs.scripts] 180 | build = [ 181 | """python -c "import glob; import subprocess; subprocess.call(['proselint'] + glob.glob('docs/*.rst'))" """, 182 | """python -c "from shutil import rmtree; rmtree('docs/build', ignore_errors=True)" """, 183 | "sphinx-build -d docs/build/tree docs docs/build --color -b html", 184 | """python -c "from pathlib import Path; p=(Path('docs')/'build'/'index.html').absolute().as_uri(); print('Documentation built under '+p)" """, 185 | ] 186 | 187 | [tool.hatch.envs.readme] 188 | template = "readme" 189 | installer = "uv" 190 | description = "check that the long description is valid" 191 | python = "3.12" 192 | dependencies = [ 193 | "build[uv]>=1.2.2", 194 | "twine>=5.1.1", 195 | "check-wheel-contents>=0.6", 196 | ] 197 | scripts = { "run" = [ 198 | "pyproject-build --installer uv -o dist", 199 | "twine check dist/*.whl dist/*.tar.gz", 200 | "check-wheel-contents dist", 201 | ] } 202 | 203 | [tool.ruff] 204 | line-length = 120 205 | format.preview = true 206 | lint.select = [ 207 | "ALL", 208 | ] 209 | lint.ignore = [ 210 | "COM812", # conflict 211 | "CPY", # no copyright notices 212 | "D203", # `one-blank-line-before-class` (D203) and `no-blank-line-before-class` (D211) are incompatible 213 | "D205", # 1 blank line required between summary line and description 214 | "D212", # `multi-line-summary-first-line` (D212) and `multi-line-summary-second-line` (D213) are incompatible 215 | "D301", # Use `r"""` if any backslashes in a docstring 216 | "D401", # The first line of docstring should be in imperative mood 217 | "DOC", # no support for restructuredtext 218 | "S104", # Possible binding to all interfaces 219 | ] 220 | lint.per-file-ignores."tests/**/*.py" = [ 221 | "D", # don't care about documentation in tests 222 | "FBT", # don't care about booleans as positional arguments in tests 223 | "INP001", # no implicit namespace 224 | "PLC2701", # Private name import 225 | "PLR0917", # Too many positional arguments 226 | "PLR2004", # Magic value used in comparison, consider replacing with a constant variable 227 | "S101", # asserts allowed in tests 228 | "S603", # `subprocess` call: check for execution of untrusted input 229 | ] 230 | 231 | lint.isort = { known-first-party = [ 232 | "platformdirs", 233 | "tests", 234 | ], required-imports = [ 235 | "from __future__ import annotations", 236 | ] } 237 | lint.preview = true 238 | 239 | [tool.codespell] 240 | builtin = "clear,usage,en-GB_to_en-US" 241 | count = true 242 | quiet-level = 3 243 | 244 | [tool.pyproject-fmt] 245 | max_supported_python = "3.13" 246 | 247 | [tool.coverage] 248 | html.show_contexts = true 249 | html.skip_covered = false 250 | run.relative_files = true 251 | paths.source = [ 252 | "src", 253 | "**/site-packages", 254 | ] 255 | report.fail_under = 76 256 | run.parallel = true 257 | run.plugins = [ 258 | "covdefaults", 259 | ] 260 | 261 | [tool.mypy] 262 | python_version = "3.11" 263 | show_error_codes = true 264 | strict = true 265 | overrides = [ 266 | { module = [ 267 | "appdirs.*", 268 | "jnius.*", 269 | ], ignore_missing_imports = true }, 270 | ] 271 | -------------------------------------------------------------------------------- /src/platformdirs/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utilities for determining application-specific dirs. 3 | 4 | See for details and usage. 5 | 6 | """ 7 | 8 | from __future__ import annotations 9 | 10 | import os 11 | import sys 12 | from typing import TYPE_CHECKING 13 | 14 | from .api import PlatformDirsABC 15 | from .version import __version__ 16 | from .version import __version_tuple__ as __version_info__ 17 | 18 | if TYPE_CHECKING: 19 | from pathlib import Path 20 | from typing import Literal 21 | 22 | if sys.platform == "win32": 23 | from platformdirs.windows import Windows as _Result 24 | elif sys.platform == "darwin": 25 | from platformdirs.macos import MacOS as _Result 26 | else: 27 | from platformdirs.unix import Unix as _Result 28 | 29 | 30 | def _set_platform_dir_class() -> type[PlatformDirsABC]: 31 | if os.getenv("ANDROID_DATA") == "/data" and os.getenv("ANDROID_ROOT") == "/system": 32 | if os.getenv("SHELL") or os.getenv("PREFIX"): 33 | return _Result 34 | 35 | from platformdirs.android import _android_folder # noqa: PLC0415 36 | 37 | if _android_folder() is not None: 38 | from platformdirs.android import Android # noqa: PLC0415 39 | 40 | return Android # return to avoid redefinition of a result 41 | 42 | return _Result 43 | 44 | 45 | if TYPE_CHECKING: 46 | # Work around mypy issue: https://github.com/python/mypy/issues/10962 47 | PlatformDirs = _Result 48 | else: 49 | PlatformDirs = _set_platform_dir_class() #: Currently active platform 50 | AppDirs = PlatformDirs #: Backwards compatibility with appdirs 51 | 52 | 53 | def user_data_dir( 54 | appname: str | None = None, 55 | appauthor: str | Literal[False] | None = None, 56 | version: str | None = None, 57 | roaming: bool = False, # noqa: FBT001, FBT002 58 | ensure_exists: bool = False, # noqa: FBT001, FBT002 59 | ) -> str: 60 | """ 61 | :param appname: See `appname `. 62 | :param appauthor: See `appauthor `. 63 | :param version: See `version `. 64 | :param roaming: See `roaming `. 65 | :param ensure_exists: See `ensure_exists `. 66 | :returns: data directory tied to the user 67 | """ 68 | return PlatformDirs( 69 | appname=appname, 70 | appauthor=appauthor, 71 | version=version, 72 | roaming=roaming, 73 | ensure_exists=ensure_exists, 74 | ).user_data_dir 75 | 76 | 77 | def site_data_dir( 78 | appname: str | None = None, 79 | appauthor: str | Literal[False] | None = None, 80 | version: str | None = None, 81 | multipath: bool = False, # noqa: FBT001, FBT002 82 | ensure_exists: bool = False, # noqa: FBT001, FBT002 83 | ) -> str: 84 | """ 85 | :param appname: See `appname `. 86 | :param appauthor: See `appauthor `. 87 | :param version: See `version `. 88 | :param multipath: See `roaming `. 89 | :param ensure_exists: See `ensure_exists `. 90 | :returns: data directory shared by users 91 | """ 92 | return PlatformDirs( 93 | appname=appname, 94 | appauthor=appauthor, 95 | version=version, 96 | multipath=multipath, 97 | ensure_exists=ensure_exists, 98 | ).site_data_dir 99 | 100 | 101 | def user_config_dir( 102 | appname: str | None = None, 103 | appauthor: str | Literal[False] | None = None, 104 | version: str | None = None, 105 | roaming: bool = False, # noqa: FBT001, FBT002 106 | ensure_exists: bool = False, # noqa: FBT001, FBT002 107 | ) -> str: 108 | """ 109 | :param appname: See `appname `. 110 | :param appauthor: See `appauthor `. 111 | :param version: See `version `. 112 | :param roaming: See `roaming `. 113 | :param ensure_exists: See `ensure_exists `. 114 | :returns: config directory tied to the user 115 | """ 116 | return PlatformDirs( 117 | appname=appname, 118 | appauthor=appauthor, 119 | version=version, 120 | roaming=roaming, 121 | ensure_exists=ensure_exists, 122 | ).user_config_dir 123 | 124 | 125 | def site_config_dir( 126 | appname: str | None = None, 127 | appauthor: str | Literal[False] | None = None, 128 | version: str | None = None, 129 | multipath: bool = False, # noqa: FBT001, FBT002 130 | ensure_exists: bool = False, # noqa: FBT001, FBT002 131 | ) -> str: 132 | """ 133 | :param appname: See `appname `. 134 | :param appauthor: See `appauthor `. 135 | :param version: See `version `. 136 | :param multipath: See `roaming `. 137 | :param ensure_exists: See `ensure_exists `. 138 | :returns: config directory shared by the users 139 | """ 140 | return PlatformDirs( 141 | appname=appname, 142 | appauthor=appauthor, 143 | version=version, 144 | multipath=multipath, 145 | ensure_exists=ensure_exists, 146 | ).site_config_dir 147 | 148 | 149 | def user_cache_dir( 150 | appname: str | None = None, 151 | appauthor: str | Literal[False] | None = None, 152 | version: str | None = None, 153 | opinion: bool = True, # noqa: FBT001, FBT002 154 | ensure_exists: bool = False, # noqa: FBT001, FBT002 155 | ) -> str: 156 | """ 157 | :param appname: See `appname `. 158 | :param appauthor: See `appauthor `. 159 | :param version: See `version `. 160 | :param opinion: See `roaming `. 161 | :param ensure_exists: See `ensure_exists `. 162 | :returns: cache directory tied to the user 163 | """ 164 | return PlatformDirs( 165 | appname=appname, 166 | appauthor=appauthor, 167 | version=version, 168 | opinion=opinion, 169 | ensure_exists=ensure_exists, 170 | ).user_cache_dir 171 | 172 | 173 | def site_cache_dir( 174 | appname: str | None = None, 175 | appauthor: str | Literal[False] | None = None, 176 | version: str | None = None, 177 | opinion: bool = True, # noqa: FBT001, FBT002 178 | ensure_exists: bool = False, # noqa: FBT001, FBT002 179 | ) -> str: 180 | """ 181 | :param appname: See `appname `. 182 | :param appauthor: See `appauthor `. 183 | :param version: See `version `. 184 | :param opinion: See `opinion `. 185 | :param ensure_exists: See `ensure_exists `. 186 | :returns: cache directory tied to the user 187 | """ 188 | return PlatformDirs( 189 | appname=appname, 190 | appauthor=appauthor, 191 | version=version, 192 | opinion=opinion, 193 | ensure_exists=ensure_exists, 194 | ).site_cache_dir 195 | 196 | 197 | def user_state_dir( 198 | appname: str | None = None, 199 | appauthor: str | Literal[False] | None = None, 200 | version: str | None = None, 201 | roaming: bool = False, # noqa: FBT001, FBT002 202 | ensure_exists: bool = False, # noqa: FBT001, FBT002 203 | ) -> str: 204 | """ 205 | :param appname: See `appname `. 206 | :param appauthor: See `appauthor `. 207 | :param version: See `version `. 208 | :param roaming: See `roaming `. 209 | :param ensure_exists: See `ensure_exists `. 210 | :returns: state directory tied to the user 211 | """ 212 | return PlatformDirs( 213 | appname=appname, 214 | appauthor=appauthor, 215 | version=version, 216 | roaming=roaming, 217 | ensure_exists=ensure_exists, 218 | ).user_state_dir 219 | 220 | 221 | def user_log_dir( 222 | appname: str | None = None, 223 | appauthor: str | Literal[False] | None = None, 224 | version: str | None = None, 225 | opinion: bool = True, # noqa: FBT001, FBT002 226 | ensure_exists: bool = False, # noqa: FBT001, FBT002 227 | ) -> str: 228 | """ 229 | :param appname: See `appname `. 230 | :param appauthor: See `appauthor `. 231 | :param version: See `version `. 232 | :param opinion: See `roaming `. 233 | :param ensure_exists: See `ensure_exists `. 234 | :returns: log directory tied to the user 235 | """ 236 | return PlatformDirs( 237 | appname=appname, 238 | appauthor=appauthor, 239 | version=version, 240 | opinion=opinion, 241 | ensure_exists=ensure_exists, 242 | ).user_log_dir 243 | 244 | 245 | def user_documents_dir() -> str: 246 | """:returns: documents directory tied to the user""" 247 | return PlatformDirs().user_documents_dir 248 | 249 | 250 | def user_downloads_dir() -> str: 251 | """:returns: downloads directory tied to the user""" 252 | return PlatformDirs().user_downloads_dir 253 | 254 | 255 | def user_pictures_dir() -> str: 256 | """:returns: pictures directory tied to the user""" 257 | return PlatformDirs().user_pictures_dir 258 | 259 | 260 | def user_videos_dir() -> str: 261 | """:returns: videos directory tied to the user""" 262 | return PlatformDirs().user_videos_dir 263 | 264 | 265 | def user_music_dir() -> str: 266 | """:returns: music directory tied to the user""" 267 | return PlatformDirs().user_music_dir 268 | 269 | 270 | def user_desktop_dir() -> str: 271 | """:returns: desktop directory tied to the user""" 272 | return PlatformDirs().user_desktop_dir 273 | 274 | 275 | def user_runtime_dir( 276 | appname: str | None = None, 277 | appauthor: str | Literal[False] | None = None, 278 | version: str | None = None, 279 | opinion: bool = True, # noqa: FBT001, FBT002 280 | ensure_exists: bool = False, # noqa: FBT001, FBT002 281 | ) -> str: 282 | """ 283 | :param appname: See `appname `. 284 | :param appauthor: See `appauthor `. 285 | :param version: See `version `. 286 | :param opinion: See `opinion `. 287 | :param ensure_exists: See `ensure_exists `. 288 | :returns: runtime directory tied to the user 289 | """ 290 | return PlatformDirs( 291 | appname=appname, 292 | appauthor=appauthor, 293 | version=version, 294 | opinion=opinion, 295 | ensure_exists=ensure_exists, 296 | ).user_runtime_dir 297 | 298 | 299 | def site_runtime_dir( 300 | appname: str | None = None, 301 | appauthor: str | Literal[False] | None = None, 302 | version: str | None = None, 303 | opinion: bool = True, # noqa: FBT001, FBT002 304 | ensure_exists: bool = False, # noqa: FBT001, FBT002 305 | ) -> str: 306 | """ 307 | :param appname: See `appname `. 308 | :param appauthor: See `appauthor `. 309 | :param version: See `version `. 310 | :param opinion: See `opinion `. 311 | :param ensure_exists: See `ensure_exists `. 312 | :returns: runtime directory shared by users 313 | """ 314 | return PlatformDirs( 315 | appname=appname, 316 | appauthor=appauthor, 317 | version=version, 318 | opinion=opinion, 319 | ensure_exists=ensure_exists, 320 | ).site_runtime_dir 321 | 322 | 323 | def user_data_path( 324 | appname: str | None = None, 325 | appauthor: str | Literal[False] | None = None, 326 | version: str | None = None, 327 | roaming: bool = False, # noqa: FBT001, FBT002 328 | ensure_exists: bool = False, # noqa: FBT001, FBT002 329 | ) -> Path: 330 | """ 331 | :param appname: See `appname `. 332 | :param appauthor: See `appauthor `. 333 | :param version: See `version `. 334 | :param roaming: See `roaming `. 335 | :param ensure_exists: See `ensure_exists `. 336 | :returns: data path tied to the user 337 | """ 338 | return PlatformDirs( 339 | appname=appname, 340 | appauthor=appauthor, 341 | version=version, 342 | roaming=roaming, 343 | ensure_exists=ensure_exists, 344 | ).user_data_path 345 | 346 | 347 | def site_data_path( 348 | appname: str | None = None, 349 | appauthor: str | Literal[False] | None = None, 350 | version: str | None = None, 351 | multipath: bool = False, # noqa: FBT001, FBT002 352 | ensure_exists: bool = False, # noqa: FBT001, FBT002 353 | ) -> Path: 354 | """ 355 | :param appname: See `appname `. 356 | :param appauthor: See `appauthor `. 357 | :param version: See `version `. 358 | :param multipath: See `multipath `. 359 | :param ensure_exists: See `ensure_exists `. 360 | :returns: data path shared by users 361 | """ 362 | return PlatformDirs( 363 | appname=appname, 364 | appauthor=appauthor, 365 | version=version, 366 | multipath=multipath, 367 | ensure_exists=ensure_exists, 368 | ).site_data_path 369 | 370 | 371 | def user_config_path( 372 | appname: str | None = None, 373 | appauthor: str | Literal[False] | None = None, 374 | version: str | None = None, 375 | roaming: bool = False, # noqa: FBT001, FBT002 376 | ensure_exists: bool = False, # noqa: FBT001, FBT002 377 | ) -> Path: 378 | """ 379 | :param appname: See `appname `. 380 | :param appauthor: See `appauthor `. 381 | :param version: See `version `. 382 | :param roaming: See `roaming `. 383 | :param ensure_exists: See `ensure_exists `. 384 | :returns: config path tied to the user 385 | """ 386 | return PlatformDirs( 387 | appname=appname, 388 | appauthor=appauthor, 389 | version=version, 390 | roaming=roaming, 391 | ensure_exists=ensure_exists, 392 | ).user_config_path 393 | 394 | 395 | def site_config_path( 396 | appname: str | None = None, 397 | appauthor: str | Literal[False] | None = None, 398 | version: str | None = None, 399 | multipath: bool = False, # noqa: FBT001, FBT002 400 | ensure_exists: bool = False, # noqa: FBT001, FBT002 401 | ) -> Path: 402 | """ 403 | :param appname: See `appname `. 404 | :param appauthor: See `appauthor `. 405 | :param version: See `version `. 406 | :param multipath: See `roaming `. 407 | :param ensure_exists: See `ensure_exists `. 408 | :returns: config path shared by the users 409 | """ 410 | return PlatformDirs( 411 | appname=appname, 412 | appauthor=appauthor, 413 | version=version, 414 | multipath=multipath, 415 | ensure_exists=ensure_exists, 416 | ).site_config_path 417 | 418 | 419 | def site_cache_path( 420 | appname: str | None = None, 421 | appauthor: str | Literal[False] | None = None, 422 | version: str | None = None, 423 | opinion: bool = True, # noqa: FBT001, FBT002 424 | ensure_exists: bool = False, # noqa: FBT001, FBT002 425 | ) -> Path: 426 | """ 427 | :param appname: See `appname `. 428 | :param appauthor: See `appauthor `. 429 | :param version: See `version `. 430 | :param opinion: See `opinion `. 431 | :param ensure_exists: See `ensure_exists `. 432 | :returns: cache directory tied to the user 433 | """ 434 | return PlatformDirs( 435 | appname=appname, 436 | appauthor=appauthor, 437 | version=version, 438 | opinion=opinion, 439 | ensure_exists=ensure_exists, 440 | ).site_cache_path 441 | 442 | 443 | def user_cache_path( 444 | appname: str | None = None, 445 | appauthor: str | Literal[False] | None = None, 446 | version: str | None = None, 447 | opinion: bool = True, # noqa: FBT001, FBT002 448 | ensure_exists: bool = False, # noqa: FBT001, FBT002 449 | ) -> Path: 450 | """ 451 | :param appname: See `appname `. 452 | :param appauthor: See `appauthor `. 453 | :param version: See `version `. 454 | :param opinion: See `roaming `. 455 | :param ensure_exists: See `ensure_exists `. 456 | :returns: cache path tied to the user 457 | """ 458 | return PlatformDirs( 459 | appname=appname, 460 | appauthor=appauthor, 461 | version=version, 462 | opinion=opinion, 463 | ensure_exists=ensure_exists, 464 | ).user_cache_path 465 | 466 | 467 | def user_state_path( 468 | appname: str | None = None, 469 | appauthor: str | Literal[False] | None = None, 470 | version: str | None = None, 471 | roaming: bool = False, # noqa: FBT001, FBT002 472 | ensure_exists: bool = False, # noqa: FBT001, FBT002 473 | ) -> Path: 474 | """ 475 | :param appname: See `appname `. 476 | :param appauthor: See `appauthor `. 477 | :param version: See `version `. 478 | :param roaming: See `roaming `. 479 | :param ensure_exists: See `ensure_exists `. 480 | :returns: state path tied to the user 481 | """ 482 | return PlatformDirs( 483 | appname=appname, 484 | appauthor=appauthor, 485 | version=version, 486 | roaming=roaming, 487 | ensure_exists=ensure_exists, 488 | ).user_state_path 489 | 490 | 491 | def user_log_path( 492 | appname: str | None = None, 493 | appauthor: str | Literal[False] | None = None, 494 | version: str | None = None, 495 | opinion: bool = True, # noqa: FBT001, FBT002 496 | ensure_exists: bool = False, # noqa: FBT001, FBT002 497 | ) -> Path: 498 | """ 499 | :param appname: See `appname `. 500 | :param appauthor: See `appauthor `. 501 | :param version: See `version `. 502 | :param opinion: See `roaming `. 503 | :param ensure_exists: See `ensure_exists `. 504 | :returns: log path tied to the user 505 | """ 506 | return PlatformDirs( 507 | appname=appname, 508 | appauthor=appauthor, 509 | version=version, 510 | opinion=opinion, 511 | ensure_exists=ensure_exists, 512 | ).user_log_path 513 | 514 | 515 | def user_documents_path() -> Path: 516 | """:returns: documents a path tied to the user""" 517 | return PlatformDirs().user_documents_path 518 | 519 | 520 | def user_downloads_path() -> Path: 521 | """:returns: downloads path tied to the user""" 522 | return PlatformDirs().user_downloads_path 523 | 524 | 525 | def user_pictures_path() -> Path: 526 | """:returns: pictures path tied to the user""" 527 | return PlatformDirs().user_pictures_path 528 | 529 | 530 | def user_videos_path() -> Path: 531 | """:returns: videos path tied to the user""" 532 | return PlatformDirs().user_videos_path 533 | 534 | 535 | def user_music_path() -> Path: 536 | """:returns: music path tied to the user""" 537 | return PlatformDirs().user_music_path 538 | 539 | 540 | def user_desktop_path() -> Path: 541 | """:returns: desktop path tied to the user""" 542 | return PlatformDirs().user_desktop_path 543 | 544 | 545 | def user_runtime_path( 546 | appname: str | None = None, 547 | appauthor: str | Literal[False] | None = None, 548 | version: str | None = None, 549 | opinion: bool = True, # noqa: FBT001, FBT002 550 | ensure_exists: bool = False, # noqa: FBT001, FBT002 551 | ) -> Path: 552 | """ 553 | :param appname: See `appname `. 554 | :param appauthor: See `appauthor `. 555 | :param version: See `version `. 556 | :param opinion: See `opinion `. 557 | :param ensure_exists: See `ensure_exists `. 558 | :returns: runtime path tied to the user 559 | """ 560 | return PlatformDirs( 561 | appname=appname, 562 | appauthor=appauthor, 563 | version=version, 564 | opinion=opinion, 565 | ensure_exists=ensure_exists, 566 | ).user_runtime_path 567 | 568 | 569 | def site_runtime_path( 570 | appname: str | None = None, 571 | appauthor: str | Literal[False] | None = None, 572 | version: str | None = None, 573 | opinion: bool = True, # noqa: FBT001, FBT002 574 | ensure_exists: bool = False, # noqa: FBT001, FBT002 575 | ) -> Path: 576 | """ 577 | :param appname: See `appname `. 578 | :param appauthor: See `appauthor `. 579 | :param version: See `version `. 580 | :param opinion: See `opinion `. 581 | :param ensure_exists: See `ensure_exists `. 582 | :returns: runtime path shared by users 583 | """ 584 | return PlatformDirs( 585 | appname=appname, 586 | appauthor=appauthor, 587 | version=version, 588 | opinion=opinion, 589 | ensure_exists=ensure_exists, 590 | ).site_runtime_path 591 | 592 | 593 | __all__ = [ 594 | "AppDirs", 595 | "PlatformDirs", 596 | "PlatformDirsABC", 597 | "__version__", 598 | "__version_info__", 599 | "site_cache_dir", 600 | "site_cache_path", 601 | "site_config_dir", 602 | "site_config_path", 603 | "site_data_dir", 604 | "site_data_path", 605 | "site_runtime_dir", 606 | "site_runtime_path", 607 | "user_cache_dir", 608 | "user_cache_path", 609 | "user_config_dir", 610 | "user_config_path", 611 | "user_data_dir", 612 | "user_data_path", 613 | "user_desktop_dir", 614 | "user_desktop_path", 615 | "user_documents_dir", 616 | "user_documents_path", 617 | "user_downloads_dir", 618 | "user_downloads_path", 619 | "user_log_dir", 620 | "user_log_path", 621 | "user_music_dir", 622 | "user_music_path", 623 | "user_pictures_dir", 624 | "user_pictures_path", 625 | "user_runtime_dir", 626 | "user_runtime_path", 627 | "user_state_dir", 628 | "user_state_path", 629 | "user_videos_dir", 630 | "user_videos_path", 631 | ] 632 | -------------------------------------------------------------------------------- /src/platformdirs/__main__.py: -------------------------------------------------------------------------------- 1 | """Main entry point.""" 2 | 3 | from __future__ import annotations 4 | 5 | from platformdirs import PlatformDirs, __version__ 6 | 7 | PROPS = ( 8 | "user_data_dir", 9 | "user_config_dir", 10 | "user_cache_dir", 11 | "user_state_dir", 12 | "user_log_dir", 13 | "user_documents_dir", 14 | "user_downloads_dir", 15 | "user_pictures_dir", 16 | "user_videos_dir", 17 | "user_music_dir", 18 | "user_runtime_dir", 19 | "site_data_dir", 20 | "site_config_dir", 21 | "site_cache_dir", 22 | "site_runtime_dir", 23 | ) 24 | 25 | 26 | def main() -> None: 27 | """Run the main entry point.""" 28 | app_name = "MyApp" 29 | app_author = "MyCompany" 30 | 31 | print(f"-- platformdirs {__version__} --") # noqa: T201 32 | 33 | print("-- app dirs (with optional 'version')") # noqa: T201 34 | dirs = PlatformDirs(app_name, app_author, version="1.0") 35 | for prop in PROPS: 36 | print(f"{prop}: {getattr(dirs, prop)}") # noqa: T201 37 | 38 | print("\n-- app dirs (without optional 'version')") # noqa: T201 39 | dirs = PlatformDirs(app_name, app_author) 40 | for prop in PROPS: 41 | print(f"{prop}: {getattr(dirs, prop)}") # noqa: T201 42 | 43 | print("\n-- app dirs (without optional 'appauthor')") # noqa: T201 44 | dirs = PlatformDirs(app_name) 45 | for prop in PROPS: 46 | print(f"{prop}: {getattr(dirs, prop)}") # noqa: T201 47 | 48 | print("\n-- app dirs (with disabled 'appauthor')") # noqa: T201 49 | dirs = PlatformDirs(app_name, appauthor=False) 50 | for prop in PROPS: 51 | print(f"{prop}: {getattr(dirs, prop)}") # noqa: T201 52 | 53 | 54 | if __name__ == "__main__": 55 | main() 56 | -------------------------------------------------------------------------------- /src/platformdirs/android.py: -------------------------------------------------------------------------------- 1 | """Android.""" 2 | 3 | from __future__ import annotations 4 | 5 | import os 6 | import re 7 | import sys 8 | from functools import lru_cache 9 | from typing import TYPE_CHECKING, cast 10 | 11 | from .api import PlatformDirsABC 12 | 13 | 14 | class Android(PlatformDirsABC): 15 | """ 16 | Follows the guidance `from here `_. 17 | 18 | Makes use of the `appname `, `version 19 | `, `ensure_exists `. 20 | 21 | """ 22 | 23 | @property 24 | def user_data_dir(self) -> str: 25 | """:return: data directory tied to the user, e.g. ``/data/user///files/``""" 26 | return self._append_app_name_and_version(cast("str", _android_folder()), "files") 27 | 28 | @property 29 | def site_data_dir(self) -> str: 30 | """:return: data directory shared by users, same as `user_data_dir`""" 31 | return self.user_data_dir 32 | 33 | @property 34 | def user_config_dir(self) -> str: 35 | """ 36 | :return: config directory tied to the user, e.g. \ 37 | ``/data/user///shared_prefs/`` 38 | """ 39 | return self._append_app_name_and_version(cast("str", _android_folder()), "shared_prefs") 40 | 41 | @property 42 | def site_config_dir(self) -> str: 43 | """:return: config directory shared by the users, same as `user_config_dir`""" 44 | return self.user_config_dir 45 | 46 | @property 47 | def user_cache_dir(self) -> str: 48 | """:return: cache directory tied to the user, e.g.,``/data/user///cache/``""" 49 | return self._append_app_name_and_version(cast("str", _android_folder()), "cache") 50 | 51 | @property 52 | def site_cache_dir(self) -> str: 53 | """:return: cache directory shared by users, same as `user_cache_dir`""" 54 | return self.user_cache_dir 55 | 56 | @property 57 | def user_state_dir(self) -> str: 58 | """:return: state directory tied to the user, same as `user_data_dir`""" 59 | return self.user_data_dir 60 | 61 | @property 62 | def user_log_dir(self) -> str: 63 | """ 64 | :return: log directory tied to the user, same as `user_cache_dir` if not opinionated else ``log`` in it, 65 | e.g. ``/data/user///cache//log`` 66 | """ 67 | path = self.user_cache_dir 68 | if self.opinion: 69 | path = os.path.join(path, "log") # noqa: PTH118 70 | return path 71 | 72 | @property 73 | def user_documents_dir(self) -> str: 74 | """:return: documents directory tied to the user e.g. ``/storage/emulated/0/Documents``""" 75 | return _android_documents_folder() 76 | 77 | @property 78 | def user_downloads_dir(self) -> str: 79 | """:return: downloads directory tied to the user e.g. ``/storage/emulated/0/Downloads``""" 80 | return _android_downloads_folder() 81 | 82 | @property 83 | def user_pictures_dir(self) -> str: 84 | """:return: pictures directory tied to the user e.g. ``/storage/emulated/0/Pictures``""" 85 | return _android_pictures_folder() 86 | 87 | @property 88 | def user_videos_dir(self) -> str: 89 | """:return: videos directory tied to the user e.g. ``/storage/emulated/0/DCIM/Camera``""" 90 | return _android_videos_folder() 91 | 92 | @property 93 | def user_music_dir(self) -> str: 94 | """:return: music directory tied to the user e.g. ``/storage/emulated/0/Music``""" 95 | return _android_music_folder() 96 | 97 | @property 98 | def user_desktop_dir(self) -> str: 99 | """:return: desktop directory tied to the user e.g. ``/storage/emulated/0/Desktop``""" 100 | return "/storage/emulated/0/Desktop" 101 | 102 | @property 103 | def user_runtime_dir(self) -> str: 104 | """ 105 | :return: runtime directory tied to the user, same as `user_cache_dir` if not opinionated else ``tmp`` in it, 106 | e.g. ``/data/user///cache//tmp`` 107 | """ 108 | path = self.user_cache_dir 109 | if self.opinion: 110 | path = os.path.join(path, "tmp") # noqa: PTH118 111 | return path 112 | 113 | @property 114 | def site_runtime_dir(self) -> str: 115 | """:return: runtime directory shared by users, same as `user_runtime_dir`""" 116 | return self.user_runtime_dir 117 | 118 | 119 | @lru_cache(maxsize=1) 120 | def _android_folder() -> str | None: # noqa: C901 121 | """:return: base folder for the Android OS or None if it cannot be found""" 122 | result: str | None = None 123 | # type checker isn't happy with our "import android", just don't do this when type checking see 124 | # https://stackoverflow.com/a/61394121 125 | if not TYPE_CHECKING: 126 | try: 127 | # First try to get a path to android app using python4android (if available)... 128 | from android import mActivity # noqa: PLC0415 129 | 130 | context = cast("android.content.Context", mActivity.getApplicationContext()) # noqa: F821 131 | result = context.getFilesDir().getParentFile().getAbsolutePath() 132 | except Exception: # noqa: BLE001 133 | result = None 134 | if result is None: 135 | try: 136 | # ...and fall back to using plain pyjnius, if python4android isn't available or doesn't deliver any useful 137 | # result... 138 | from jnius import autoclass # noqa: PLC0415 139 | 140 | context = autoclass("android.content.Context") 141 | result = context.getFilesDir().getParentFile().getAbsolutePath() 142 | except Exception: # noqa: BLE001 143 | result = None 144 | if result is None: 145 | # and if that fails, too, find an android folder looking at path on the sys.path 146 | # warning: only works for apps installed under /data, not adopted storage etc. 147 | pattern = re.compile(r"/data/(data|user/\d+)/(.+)/files") 148 | for path in sys.path: 149 | if pattern.match(path): 150 | result = path.split("/files")[0] 151 | break 152 | else: 153 | result = None 154 | if result is None: 155 | # one last try: find an android folder looking at path on the sys.path taking adopted storage paths into 156 | # account 157 | pattern = re.compile(r"/mnt/expand/[a-fA-F0-9-]{36}/(data|user/\d+)/(.+)/files") 158 | for path in sys.path: 159 | if pattern.match(path): 160 | result = path.split("/files")[0] 161 | break 162 | else: 163 | result = None 164 | return result 165 | 166 | 167 | @lru_cache(maxsize=1) 168 | def _android_documents_folder() -> str: 169 | """:return: documents folder for the Android OS""" 170 | # Get directories with pyjnius 171 | try: 172 | from jnius import autoclass # noqa: PLC0415 173 | 174 | context = autoclass("android.content.Context") 175 | environment = autoclass("android.os.Environment") 176 | documents_dir: str = context.getExternalFilesDir(environment.DIRECTORY_DOCUMENTS).getAbsolutePath() 177 | except Exception: # noqa: BLE001 178 | documents_dir = "/storage/emulated/0/Documents" 179 | 180 | return documents_dir 181 | 182 | 183 | @lru_cache(maxsize=1) 184 | def _android_downloads_folder() -> str: 185 | """:return: downloads folder for the Android OS""" 186 | # Get directories with pyjnius 187 | try: 188 | from jnius import autoclass # noqa: PLC0415 189 | 190 | context = autoclass("android.content.Context") 191 | environment = autoclass("android.os.Environment") 192 | downloads_dir: str = context.getExternalFilesDir(environment.DIRECTORY_DOWNLOADS).getAbsolutePath() 193 | except Exception: # noqa: BLE001 194 | downloads_dir = "/storage/emulated/0/Downloads" 195 | 196 | return downloads_dir 197 | 198 | 199 | @lru_cache(maxsize=1) 200 | def _android_pictures_folder() -> str: 201 | """:return: pictures folder for the Android OS""" 202 | # Get directories with pyjnius 203 | try: 204 | from jnius import autoclass # noqa: PLC0415 205 | 206 | context = autoclass("android.content.Context") 207 | environment = autoclass("android.os.Environment") 208 | pictures_dir: str = context.getExternalFilesDir(environment.DIRECTORY_PICTURES).getAbsolutePath() 209 | except Exception: # noqa: BLE001 210 | pictures_dir = "/storage/emulated/0/Pictures" 211 | 212 | return pictures_dir 213 | 214 | 215 | @lru_cache(maxsize=1) 216 | def _android_videos_folder() -> str: 217 | """:return: videos folder for the Android OS""" 218 | # Get directories with pyjnius 219 | try: 220 | from jnius import autoclass # noqa: PLC0415 221 | 222 | context = autoclass("android.content.Context") 223 | environment = autoclass("android.os.Environment") 224 | videos_dir: str = context.getExternalFilesDir(environment.DIRECTORY_DCIM).getAbsolutePath() 225 | except Exception: # noqa: BLE001 226 | videos_dir = "/storage/emulated/0/DCIM/Camera" 227 | 228 | return videos_dir 229 | 230 | 231 | @lru_cache(maxsize=1) 232 | def _android_music_folder() -> str: 233 | """:return: music folder for the Android OS""" 234 | # Get directories with pyjnius 235 | try: 236 | from jnius import autoclass # noqa: PLC0415 237 | 238 | context = autoclass("android.content.Context") 239 | environment = autoclass("android.os.Environment") 240 | music_dir: str = context.getExternalFilesDir(environment.DIRECTORY_MUSIC).getAbsolutePath() 241 | except Exception: # noqa: BLE001 242 | music_dir = "/storage/emulated/0/Music" 243 | 244 | return music_dir 245 | 246 | 247 | __all__ = [ 248 | "Android", 249 | ] 250 | -------------------------------------------------------------------------------- /src/platformdirs/api.py: -------------------------------------------------------------------------------- 1 | """Base API.""" 2 | 3 | from __future__ import annotations 4 | 5 | import os 6 | from abc import ABC, abstractmethod 7 | from pathlib import Path 8 | from typing import TYPE_CHECKING 9 | 10 | if TYPE_CHECKING: 11 | from collections.abc import Iterator 12 | from typing import Literal 13 | 14 | 15 | class PlatformDirsABC(ABC): # noqa: PLR0904 16 | """Abstract base class for platform directories.""" 17 | 18 | def __init__( # noqa: PLR0913, PLR0917 19 | self, 20 | appname: str | None = None, 21 | appauthor: str | Literal[False] | None = None, 22 | version: str | None = None, 23 | roaming: bool = False, # noqa: FBT001, FBT002 24 | multipath: bool = False, # noqa: FBT001, FBT002 25 | opinion: bool = True, # noqa: FBT001, FBT002 26 | ensure_exists: bool = False, # noqa: FBT001, FBT002 27 | ) -> None: 28 | """ 29 | Create a new platform directory. 30 | 31 | :param appname: See `appname`. 32 | :param appauthor: See `appauthor`. 33 | :param version: See `version`. 34 | :param roaming: See `roaming`. 35 | :param multipath: See `multipath`. 36 | :param opinion: See `opinion`. 37 | :param ensure_exists: See `ensure_exists`. 38 | 39 | """ 40 | self.appname = appname #: The name of application. 41 | self.appauthor = appauthor 42 | """ 43 | The name of the app author or distributing body for this application. 44 | 45 | Typically, it is the owning company name. Defaults to `appname`. You may pass ``False`` to disable it. 46 | 47 | """ 48 | self.version = version 49 | """ 50 | An optional version path element to append to the path. 51 | 52 | You might want to use this if you want multiple versions of your app to be able to run independently. If used, 53 | this would typically be ``.``. 54 | 55 | """ 56 | self.roaming = roaming 57 | """ 58 | Whether to use the roaming appdata directory on Windows. 59 | 60 | That means that for users on a Windows network setup for roaming profiles, this user data will be synced on 61 | login (see 62 | `here `_). 63 | 64 | """ 65 | self.multipath = multipath 66 | """ 67 | An optional parameter which indicates that the entire list of data dirs should be returned. 68 | 69 | By default, the first item would only be returned. 70 | 71 | """ 72 | self.opinion = opinion #: A flag to indicating to use opinionated values. 73 | self.ensure_exists = ensure_exists 74 | """ 75 | Optionally create the directory (and any missing parents) upon access if it does not exist. 76 | 77 | By default, no directories are created. 78 | 79 | """ 80 | 81 | def _append_app_name_and_version(self, *base: str) -> str: 82 | params = list(base[1:]) 83 | if self.appname: 84 | params.append(self.appname) 85 | if self.version: 86 | params.append(self.version) 87 | path = os.path.join(base[0], *params) # noqa: PTH118 88 | self._optionally_create_directory(path) 89 | return path 90 | 91 | def _optionally_create_directory(self, path: str) -> None: 92 | if self.ensure_exists: 93 | Path(path).mkdir(parents=True, exist_ok=True) 94 | 95 | def _first_item_as_path_if_multipath(self, directory: str) -> Path: 96 | if self.multipath: 97 | # If multipath is True, the first path is returned. 98 | directory = directory.split(os.pathsep)[0] 99 | return Path(directory) 100 | 101 | @property 102 | @abstractmethod 103 | def user_data_dir(self) -> str: 104 | """:return: data directory tied to the user""" 105 | 106 | @property 107 | @abstractmethod 108 | def site_data_dir(self) -> str: 109 | """:return: data directory shared by users""" 110 | 111 | @property 112 | @abstractmethod 113 | def user_config_dir(self) -> str: 114 | """:return: config directory tied to the user""" 115 | 116 | @property 117 | @abstractmethod 118 | def site_config_dir(self) -> str: 119 | """:return: config directory shared by the users""" 120 | 121 | @property 122 | @abstractmethod 123 | def user_cache_dir(self) -> str: 124 | """:return: cache directory tied to the user""" 125 | 126 | @property 127 | @abstractmethod 128 | def site_cache_dir(self) -> str: 129 | """:return: cache directory shared by users""" 130 | 131 | @property 132 | @abstractmethod 133 | def user_state_dir(self) -> str: 134 | """:return: state directory tied to the user""" 135 | 136 | @property 137 | @abstractmethod 138 | def user_log_dir(self) -> str: 139 | """:return: log directory tied to the user""" 140 | 141 | @property 142 | @abstractmethod 143 | def user_documents_dir(self) -> str: 144 | """:return: documents directory tied to the user""" 145 | 146 | @property 147 | @abstractmethod 148 | def user_downloads_dir(self) -> str: 149 | """:return: downloads directory tied to the user""" 150 | 151 | @property 152 | @abstractmethod 153 | def user_pictures_dir(self) -> str: 154 | """:return: pictures directory tied to the user""" 155 | 156 | @property 157 | @abstractmethod 158 | def user_videos_dir(self) -> str: 159 | """:return: videos directory tied to the user""" 160 | 161 | @property 162 | @abstractmethod 163 | def user_music_dir(self) -> str: 164 | """:return: music directory tied to the user""" 165 | 166 | @property 167 | @abstractmethod 168 | def user_desktop_dir(self) -> str: 169 | """:return: desktop directory tied to the user""" 170 | 171 | @property 172 | @abstractmethod 173 | def user_runtime_dir(self) -> str: 174 | """:return: runtime directory tied to the user""" 175 | 176 | @property 177 | @abstractmethod 178 | def site_runtime_dir(self) -> str: 179 | """:return: runtime directory shared by users""" 180 | 181 | @property 182 | def user_data_path(self) -> Path: 183 | """:return: data path tied to the user""" 184 | return Path(self.user_data_dir) 185 | 186 | @property 187 | def site_data_path(self) -> Path: 188 | """:return: data path shared by users""" 189 | return Path(self.site_data_dir) 190 | 191 | @property 192 | def user_config_path(self) -> Path: 193 | """:return: config path tied to the user""" 194 | return Path(self.user_config_dir) 195 | 196 | @property 197 | def site_config_path(self) -> Path: 198 | """:return: config path shared by the users""" 199 | return Path(self.site_config_dir) 200 | 201 | @property 202 | def user_cache_path(self) -> Path: 203 | """:return: cache path tied to the user""" 204 | return Path(self.user_cache_dir) 205 | 206 | @property 207 | def site_cache_path(self) -> Path: 208 | """:return: cache path shared by users""" 209 | return Path(self.site_cache_dir) 210 | 211 | @property 212 | def user_state_path(self) -> Path: 213 | """:return: state path tied to the user""" 214 | return Path(self.user_state_dir) 215 | 216 | @property 217 | def user_log_path(self) -> Path: 218 | """:return: log path tied to the user""" 219 | return Path(self.user_log_dir) 220 | 221 | @property 222 | def user_documents_path(self) -> Path: 223 | """:return: documents a path tied to the user""" 224 | return Path(self.user_documents_dir) 225 | 226 | @property 227 | def user_downloads_path(self) -> Path: 228 | """:return: downloads path tied to the user""" 229 | return Path(self.user_downloads_dir) 230 | 231 | @property 232 | def user_pictures_path(self) -> Path: 233 | """:return: pictures path tied to the user""" 234 | return Path(self.user_pictures_dir) 235 | 236 | @property 237 | def user_videos_path(self) -> Path: 238 | """:return: videos path tied to the user""" 239 | return Path(self.user_videos_dir) 240 | 241 | @property 242 | def user_music_path(self) -> Path: 243 | """:return: music path tied to the user""" 244 | return Path(self.user_music_dir) 245 | 246 | @property 247 | def user_desktop_path(self) -> Path: 248 | """:return: desktop path tied to the user""" 249 | return Path(self.user_desktop_dir) 250 | 251 | @property 252 | def user_runtime_path(self) -> Path: 253 | """:return: runtime path tied to the user""" 254 | return Path(self.user_runtime_dir) 255 | 256 | @property 257 | def site_runtime_path(self) -> Path: 258 | """:return: runtime path shared by users""" 259 | return Path(self.site_runtime_dir) 260 | 261 | def iter_config_dirs(self) -> Iterator[str]: 262 | """:yield: all user and site configuration directories.""" 263 | yield self.user_config_dir 264 | yield self.site_config_dir 265 | 266 | def iter_data_dirs(self) -> Iterator[str]: 267 | """:yield: all user and site data directories.""" 268 | yield self.user_data_dir 269 | yield self.site_data_dir 270 | 271 | def iter_cache_dirs(self) -> Iterator[str]: 272 | """:yield: all user and site cache directories.""" 273 | yield self.user_cache_dir 274 | yield self.site_cache_dir 275 | 276 | def iter_runtime_dirs(self) -> Iterator[str]: 277 | """:yield: all user and site runtime directories.""" 278 | yield self.user_runtime_dir 279 | yield self.site_runtime_dir 280 | 281 | def iter_config_paths(self) -> Iterator[Path]: 282 | """:yield: all user and site configuration paths.""" 283 | for path in self.iter_config_dirs(): 284 | yield Path(path) 285 | 286 | def iter_data_paths(self) -> Iterator[Path]: 287 | """:yield: all user and site data paths.""" 288 | for path in self.iter_data_dirs(): 289 | yield Path(path) 290 | 291 | def iter_cache_paths(self) -> Iterator[Path]: 292 | """:yield: all user and site cache paths.""" 293 | for path in self.iter_cache_dirs(): 294 | yield Path(path) 295 | 296 | def iter_runtime_paths(self) -> Iterator[Path]: 297 | """:yield: all user and site runtime paths.""" 298 | for path in self.iter_runtime_dirs(): 299 | yield Path(path) 300 | -------------------------------------------------------------------------------- /src/platformdirs/macos.py: -------------------------------------------------------------------------------- 1 | """macOS.""" 2 | 3 | from __future__ import annotations 4 | 5 | import os.path 6 | import sys 7 | from typing import TYPE_CHECKING 8 | 9 | from .api import PlatformDirsABC 10 | 11 | if TYPE_CHECKING: 12 | from pathlib import Path 13 | 14 | 15 | class MacOS(PlatformDirsABC): 16 | """ 17 | Platform directories for the macOS operating system. 18 | 19 | Follows the guidance from 20 | `Apple documentation `_. 21 | Makes use of the `appname `, 22 | `version `, 23 | `ensure_exists `. 24 | 25 | """ 26 | 27 | @property 28 | def user_data_dir(self) -> str: 29 | """:return: data directory tied to the user, e.g. ``~/Library/Application Support/$appname/$version``""" 30 | return self._append_app_name_and_version(os.path.expanduser("~/Library/Application Support")) # noqa: PTH111 31 | 32 | @property 33 | def site_data_dir(self) -> str: 34 | """ 35 | :return: data directory shared by users, e.g. ``/Library/Application Support/$appname/$version``. 36 | If we're using a Python binary managed by `Homebrew `_, the directory 37 | will be under the Homebrew prefix, e.g. ``/opt/homebrew/share/$appname/$version``. 38 | If `multipath ` is enabled, and we're in Homebrew, 39 | the response is a multi-path string separated by ":", e.g. 40 | ``/opt/homebrew/share/$appname/$version:/Library/Application Support/$appname/$version`` 41 | """ 42 | is_homebrew = sys.prefix.startswith("/opt/homebrew") 43 | path_list = [self._append_app_name_and_version("/opt/homebrew/share")] if is_homebrew else [] 44 | path_list.append(self._append_app_name_and_version("/Library/Application Support")) 45 | if self.multipath: 46 | return os.pathsep.join(path_list) 47 | return path_list[0] 48 | 49 | @property 50 | def site_data_path(self) -> Path: 51 | """:return: data path shared by users. Only return the first item, even if ``multipath`` is set to ``True``""" 52 | return self._first_item_as_path_if_multipath(self.site_data_dir) 53 | 54 | @property 55 | def user_config_dir(self) -> str: 56 | """:return: config directory tied to the user, same as `user_data_dir`""" 57 | return self.user_data_dir 58 | 59 | @property 60 | def site_config_dir(self) -> str: 61 | """:return: config directory shared by the users, same as `site_data_dir`""" 62 | return self.site_data_dir 63 | 64 | @property 65 | def user_cache_dir(self) -> str: 66 | """:return: cache directory tied to the user, e.g. ``~/Library/Caches/$appname/$version``""" 67 | return self._append_app_name_and_version(os.path.expanduser("~/Library/Caches")) # noqa: PTH111 68 | 69 | @property 70 | def site_cache_dir(self) -> str: 71 | """ 72 | :return: cache directory shared by users, e.g. ``/Library/Caches/$appname/$version``. 73 | If we're using a Python binary managed by `Homebrew `_, the directory 74 | will be under the Homebrew prefix, e.g. ``/opt/homebrew/var/cache/$appname/$version``. 75 | If `multipath ` is enabled, and we're in Homebrew, 76 | the response is a multi-path string separated by ":", e.g. 77 | ``/opt/homebrew/var/cache/$appname/$version:/Library/Caches/$appname/$version`` 78 | """ 79 | is_homebrew = sys.prefix.startswith("/opt/homebrew") 80 | path_list = [self._append_app_name_and_version("/opt/homebrew/var/cache")] if is_homebrew else [] 81 | path_list.append(self._append_app_name_and_version("/Library/Caches")) 82 | if self.multipath: 83 | return os.pathsep.join(path_list) 84 | return path_list[0] 85 | 86 | @property 87 | def site_cache_path(self) -> Path: 88 | """:return: cache path shared by users. Only return the first item, even if ``multipath`` is set to ``True``""" 89 | return self._first_item_as_path_if_multipath(self.site_cache_dir) 90 | 91 | @property 92 | def user_state_dir(self) -> str: 93 | """:return: state directory tied to the user, same as `user_data_dir`""" 94 | return self.user_data_dir 95 | 96 | @property 97 | def user_log_dir(self) -> str: 98 | """:return: log directory tied to the user, e.g. ``~/Library/Logs/$appname/$version``""" 99 | return self._append_app_name_and_version(os.path.expanduser("~/Library/Logs")) # noqa: PTH111 100 | 101 | @property 102 | def user_documents_dir(self) -> str: 103 | """:return: documents directory tied to the user, e.g. ``~/Documents``""" 104 | return os.path.expanduser("~/Documents") # noqa: PTH111 105 | 106 | @property 107 | def user_downloads_dir(self) -> str: 108 | """:return: downloads directory tied to the user, e.g. ``~/Downloads``""" 109 | return os.path.expanduser("~/Downloads") # noqa: PTH111 110 | 111 | @property 112 | def user_pictures_dir(self) -> str: 113 | """:return: pictures directory tied to the user, e.g. ``~/Pictures``""" 114 | return os.path.expanduser("~/Pictures") # noqa: PTH111 115 | 116 | @property 117 | def user_videos_dir(self) -> str: 118 | """:return: videos directory tied to the user, e.g. ``~/Movies``""" 119 | return os.path.expanduser("~/Movies") # noqa: PTH111 120 | 121 | @property 122 | def user_music_dir(self) -> str: 123 | """:return: music directory tied to the user, e.g. ``~/Music``""" 124 | return os.path.expanduser("~/Music") # noqa: PTH111 125 | 126 | @property 127 | def user_desktop_dir(self) -> str: 128 | """:return: desktop directory tied to the user, e.g. ``~/Desktop``""" 129 | return os.path.expanduser("~/Desktop") # noqa: PTH111 130 | 131 | @property 132 | def user_runtime_dir(self) -> str: 133 | """:return: runtime directory tied to the user, e.g. ``~/Library/Caches/TemporaryItems/$appname/$version``""" 134 | return self._append_app_name_and_version(os.path.expanduser("~/Library/Caches/TemporaryItems")) # noqa: PTH111 135 | 136 | @property 137 | def site_runtime_dir(self) -> str: 138 | """:return: runtime directory shared by users, same as `user_runtime_dir`""" 139 | return self.user_runtime_dir 140 | 141 | 142 | __all__ = [ 143 | "MacOS", 144 | ] 145 | -------------------------------------------------------------------------------- /src/platformdirs/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tox-dev/platformdirs/02a42615b7d8b9ffd08aed8fd63bf6076fe9fea6/src/platformdirs/py.typed -------------------------------------------------------------------------------- /src/platformdirs/unix.py: -------------------------------------------------------------------------------- 1 | """Unix.""" 2 | 3 | from __future__ import annotations 4 | 5 | import os 6 | import sys 7 | from configparser import ConfigParser 8 | from pathlib import Path 9 | from typing import TYPE_CHECKING, NoReturn 10 | 11 | from .api import PlatformDirsABC 12 | 13 | if TYPE_CHECKING: 14 | from collections.abc import Iterator 15 | 16 | if sys.platform == "win32": 17 | 18 | def getuid() -> NoReturn: 19 | msg = "should only be used on Unix" 20 | raise RuntimeError(msg) 21 | 22 | else: 23 | from os import getuid 24 | 25 | 26 | class Unix(PlatformDirsABC): # noqa: PLR0904 27 | """ 28 | On Unix/Linux, we follow the `XDG Basedir Spec `_. 30 | 31 | The spec allows overriding directories with environment variables. The examples shown are the default values, 32 | alongside the name of the environment variable that overrides them. Makes use of the `appname 33 | `, `version `, `multipath 34 | `, `opinion `, `ensure_exists 35 | `. 36 | 37 | """ 38 | 39 | @property 40 | def user_data_dir(self) -> str: 41 | """ 42 | :return: data directory tied to the user, e.g. ``~/.local/share/$appname/$version`` or 43 | ``$XDG_DATA_HOME/$appname/$version`` 44 | """ 45 | path = os.environ.get("XDG_DATA_HOME", "") 46 | if not path.strip(): 47 | path = os.path.expanduser("~/.local/share") # noqa: PTH111 48 | return self._append_app_name_and_version(path) 49 | 50 | @property 51 | def _site_data_dirs(self) -> list[str]: 52 | path = os.environ.get("XDG_DATA_DIRS", "") 53 | if not path.strip(): 54 | path = f"/usr/local/share{os.pathsep}/usr/share" 55 | return [self._append_app_name_and_version(p) for p in path.split(os.pathsep)] 56 | 57 | @property 58 | def site_data_dir(self) -> str: 59 | """ 60 | :return: data directories shared by users (if `multipath ` is 61 | enabled and ``XDG_DATA_DIRS`` is set and a multi path the response is also a multi path separated by the 62 | OS path separator), e.g. ``/usr/local/share/$appname/$version`` or ``/usr/share/$appname/$version`` 63 | """ 64 | # XDG default for $XDG_DATA_DIRS; only first, if multipath is False 65 | dirs = self._site_data_dirs 66 | if not self.multipath: 67 | return dirs[0] 68 | return os.pathsep.join(dirs) 69 | 70 | @property 71 | def user_config_dir(self) -> str: 72 | """ 73 | :return: config directory tied to the user, e.g. ``~/.config/$appname/$version`` or 74 | ``$XDG_CONFIG_HOME/$appname/$version`` 75 | """ 76 | path = os.environ.get("XDG_CONFIG_HOME", "") 77 | if not path.strip(): 78 | path = os.path.expanduser("~/.config") # noqa: PTH111 79 | return self._append_app_name_and_version(path) 80 | 81 | @property 82 | def _site_config_dirs(self) -> list[str]: 83 | path = os.environ.get("XDG_CONFIG_DIRS", "") 84 | if not path.strip(): 85 | path = "/etc/xdg" 86 | return [self._append_app_name_and_version(p) for p in path.split(os.pathsep)] 87 | 88 | @property 89 | def site_config_dir(self) -> str: 90 | """ 91 | :return: config directories shared by users (if `multipath ` 92 | is enabled and ``XDG_CONFIG_DIRS`` is set and a multi path the response is also a multi path separated by 93 | the OS path separator), e.g. ``/etc/xdg/$appname/$version`` 94 | """ 95 | # XDG default for $XDG_CONFIG_DIRS only first, if multipath is False 96 | dirs = self._site_config_dirs 97 | if not self.multipath: 98 | return dirs[0] 99 | return os.pathsep.join(dirs) 100 | 101 | @property 102 | def user_cache_dir(self) -> str: 103 | """ 104 | :return: cache directory tied to the user, e.g. ``~/.cache/$appname/$version`` or 105 | ``~/$XDG_CACHE_HOME/$appname/$version`` 106 | """ 107 | path = os.environ.get("XDG_CACHE_HOME", "") 108 | if not path.strip(): 109 | path = os.path.expanduser("~/.cache") # noqa: PTH111 110 | return self._append_app_name_and_version(path) 111 | 112 | @property 113 | def site_cache_dir(self) -> str: 114 | """:return: cache directory shared by users, e.g. ``/var/cache/$appname/$version``""" 115 | return self._append_app_name_and_version("/var/cache") 116 | 117 | @property 118 | def user_state_dir(self) -> str: 119 | """ 120 | :return: state directory tied to the user, e.g. ``~/.local/state/$appname/$version`` or 121 | ``$XDG_STATE_HOME/$appname/$version`` 122 | """ 123 | path = os.environ.get("XDG_STATE_HOME", "") 124 | if not path.strip(): 125 | path = os.path.expanduser("~/.local/state") # noqa: PTH111 126 | return self._append_app_name_and_version(path) 127 | 128 | @property 129 | def user_log_dir(self) -> str: 130 | """:return: log directory tied to the user, same as `user_state_dir` if not opinionated else ``log`` in it""" 131 | path = self.user_state_dir 132 | if self.opinion: 133 | path = os.path.join(path, "log") # noqa: PTH118 134 | self._optionally_create_directory(path) 135 | return path 136 | 137 | @property 138 | def user_documents_dir(self) -> str: 139 | """:return: documents directory tied to the user, e.g. ``~/Documents``""" 140 | return _get_user_media_dir("XDG_DOCUMENTS_DIR", "~/Documents") 141 | 142 | @property 143 | def user_downloads_dir(self) -> str: 144 | """:return: downloads directory tied to the user, e.g. ``~/Downloads``""" 145 | return _get_user_media_dir("XDG_DOWNLOAD_DIR", "~/Downloads") 146 | 147 | @property 148 | def user_pictures_dir(self) -> str: 149 | """:return: pictures directory tied to the user, e.g. ``~/Pictures``""" 150 | return _get_user_media_dir("XDG_PICTURES_DIR", "~/Pictures") 151 | 152 | @property 153 | def user_videos_dir(self) -> str: 154 | """:return: videos directory tied to the user, e.g. ``~/Videos``""" 155 | return _get_user_media_dir("XDG_VIDEOS_DIR", "~/Videos") 156 | 157 | @property 158 | def user_music_dir(self) -> str: 159 | """:return: music directory tied to the user, e.g. ``~/Music``""" 160 | return _get_user_media_dir("XDG_MUSIC_DIR", "~/Music") 161 | 162 | @property 163 | def user_desktop_dir(self) -> str: 164 | """:return: desktop directory tied to the user, e.g. ``~/Desktop``""" 165 | return _get_user_media_dir("XDG_DESKTOP_DIR", "~/Desktop") 166 | 167 | @property 168 | def user_runtime_dir(self) -> str: 169 | """ 170 | :return: runtime directory tied to the user, e.g. ``/run/user/$(id -u)/$appname/$version`` or 171 | ``$XDG_RUNTIME_DIR/$appname/$version``. 172 | 173 | For FreeBSD/OpenBSD/NetBSD, it would return ``/var/run/user/$(id -u)/$appname/$version`` if 174 | exists, otherwise ``/tmp/runtime-$(id -u)/$appname/$version``, if``$XDG_RUNTIME_DIR`` 175 | is not set. 176 | """ 177 | path = os.environ.get("XDG_RUNTIME_DIR", "") 178 | if not path.strip(): 179 | if sys.platform.startswith(("freebsd", "openbsd", "netbsd")): 180 | path = f"/var/run/user/{getuid()}" 181 | if not Path(path).exists(): 182 | path = f"/tmp/runtime-{getuid()}" # noqa: S108 183 | else: 184 | path = f"/run/user/{getuid()}" 185 | return self._append_app_name_and_version(path) 186 | 187 | @property 188 | def site_runtime_dir(self) -> str: 189 | """ 190 | :return: runtime directory shared by users, e.g. ``/run/$appname/$version`` or \ 191 | ``$XDG_RUNTIME_DIR/$appname/$version``. 192 | 193 | Note that this behaves almost exactly like `user_runtime_dir` if ``$XDG_RUNTIME_DIR`` is set, but will 194 | fall back to paths associated to the root user instead of a regular logged-in user if it's not set. 195 | 196 | If you wish to ensure that a logged-in root user path is returned e.g. ``/run/user/0``, use `user_runtime_dir` 197 | instead. 198 | 199 | For FreeBSD/OpenBSD/NetBSD, it would return ``/var/run/$appname/$version`` if ``$XDG_RUNTIME_DIR`` is not set. 200 | """ 201 | path = os.environ.get("XDG_RUNTIME_DIR", "") 202 | if not path.strip(): 203 | if sys.platform.startswith(("freebsd", "openbsd", "netbsd")): 204 | path = "/var/run" 205 | else: 206 | path = "/run" 207 | return self._append_app_name_and_version(path) 208 | 209 | @property 210 | def site_data_path(self) -> Path: 211 | """:return: data path shared by users. Only return the first item, even if ``multipath`` is set to ``True``""" 212 | return self._first_item_as_path_if_multipath(self.site_data_dir) 213 | 214 | @property 215 | def site_config_path(self) -> Path: 216 | """:return: config path shared by the users, returns the first item, even if ``multipath`` is set to ``True``""" 217 | return self._first_item_as_path_if_multipath(self.site_config_dir) 218 | 219 | @property 220 | def site_cache_path(self) -> Path: 221 | """:return: cache path shared by users. Only return the first item, even if ``multipath`` is set to ``True``""" 222 | return self._first_item_as_path_if_multipath(self.site_cache_dir) 223 | 224 | def iter_config_dirs(self) -> Iterator[str]: 225 | """:yield: all user and site configuration directories.""" 226 | yield self.user_config_dir 227 | yield from self._site_config_dirs 228 | 229 | def iter_data_dirs(self) -> Iterator[str]: 230 | """:yield: all user and site data directories.""" 231 | yield self.user_data_dir 232 | yield from self._site_data_dirs 233 | 234 | 235 | def _get_user_media_dir(env_var: str, fallback_tilde_path: str) -> str: 236 | media_dir = _get_user_dirs_folder(env_var) 237 | if media_dir is None: 238 | media_dir = os.environ.get(env_var, "").strip() 239 | if not media_dir: 240 | media_dir = os.path.expanduser(fallback_tilde_path) # noqa: PTH111 241 | 242 | return media_dir 243 | 244 | 245 | def _get_user_dirs_folder(key: str) -> str | None: 246 | """ 247 | Return directory from user-dirs.dirs config file. 248 | 249 | See https://freedesktop.org/wiki/Software/xdg-user-dirs/. 250 | 251 | """ 252 | user_dirs_config_path = Path(Unix().user_config_dir) / "user-dirs.dirs" 253 | if user_dirs_config_path.exists(): 254 | parser = ConfigParser() 255 | 256 | with user_dirs_config_path.open() as stream: 257 | # Add fake section header, so ConfigParser doesn't complain 258 | parser.read_string(f"[top]\n{stream.read()}") 259 | 260 | if key not in parser["top"]: 261 | return None 262 | 263 | path = parser["top"][key].strip('"') 264 | # Handle relative home paths 265 | return path.replace("$HOME", os.path.expanduser("~")) # noqa: PTH111 266 | 267 | return None 268 | 269 | 270 | __all__ = [ 271 | "Unix", 272 | ] 273 | -------------------------------------------------------------------------------- /src/platformdirs/windows.py: -------------------------------------------------------------------------------- 1 | """Windows.""" 2 | 3 | from __future__ import annotations 4 | 5 | import os 6 | import sys 7 | from functools import lru_cache 8 | from typing import TYPE_CHECKING 9 | 10 | from .api import PlatformDirsABC 11 | 12 | if TYPE_CHECKING: 13 | from collections.abc import Callable 14 | 15 | 16 | class Windows(PlatformDirsABC): 17 | """ 18 | `MSDN on where to store app data files `_. 19 | 20 | Makes use of the `appname `, `appauthor 21 | `, `version `, `roaming 22 | `, `opinion `, `ensure_exists 23 | `. 24 | 25 | """ 26 | 27 | @property 28 | def user_data_dir(self) -> str: 29 | """ 30 | :return: data directory tied to the user, e.g. 31 | ``%USERPROFILE%\\AppData\\Local\\$appauthor\\$appname`` (not roaming) or 32 | ``%USERPROFILE%\\AppData\\Roaming\\$appauthor\\$appname`` (roaming) 33 | """ 34 | const = "CSIDL_APPDATA" if self.roaming else "CSIDL_LOCAL_APPDATA" 35 | path = os.path.normpath(get_win_folder(const)) 36 | return self._append_parts(path) 37 | 38 | def _append_parts(self, path: str, *, opinion_value: str | None = None) -> str: 39 | params = [] 40 | if self.appname: 41 | if self.appauthor is not False: 42 | author = self.appauthor or self.appname 43 | params.append(author) 44 | params.append(self.appname) 45 | if opinion_value is not None and self.opinion: 46 | params.append(opinion_value) 47 | if self.version: 48 | params.append(self.version) 49 | path = os.path.join(path, *params) # noqa: PTH118 50 | self._optionally_create_directory(path) 51 | return path 52 | 53 | @property 54 | def site_data_dir(self) -> str: 55 | """:return: data directory shared by users, e.g. ``C:\\ProgramData\\$appauthor\\$appname``""" 56 | path = os.path.normpath(get_win_folder("CSIDL_COMMON_APPDATA")) 57 | return self._append_parts(path) 58 | 59 | @property 60 | def user_config_dir(self) -> str: 61 | """:return: config directory tied to the user, same as `user_data_dir`""" 62 | return self.user_data_dir 63 | 64 | @property 65 | def site_config_dir(self) -> str: 66 | """:return: config directory shared by the users, same as `site_data_dir`""" 67 | return self.site_data_dir 68 | 69 | @property 70 | def user_cache_dir(self) -> str: 71 | """ 72 | :return: cache directory tied to the user (if opinionated with ``Cache`` folder within ``$appname``) e.g. 73 | ``%USERPROFILE%\\AppData\\Local\\$appauthor\\$appname\\Cache\\$version`` 74 | """ 75 | path = os.path.normpath(get_win_folder("CSIDL_LOCAL_APPDATA")) 76 | return self._append_parts(path, opinion_value="Cache") 77 | 78 | @property 79 | def site_cache_dir(self) -> str: 80 | """:return: cache directory shared by users, e.g. ``C:\\ProgramData\\$appauthor\\$appname\\Cache\\$version``""" 81 | path = os.path.normpath(get_win_folder("CSIDL_COMMON_APPDATA")) 82 | return self._append_parts(path, opinion_value="Cache") 83 | 84 | @property 85 | def user_state_dir(self) -> str: 86 | """:return: state directory tied to the user, same as `user_data_dir`""" 87 | return self.user_data_dir 88 | 89 | @property 90 | def user_log_dir(self) -> str: 91 | """:return: log directory tied to the user, same as `user_data_dir` if not opinionated else ``Logs`` in it""" 92 | path = self.user_data_dir 93 | if self.opinion: 94 | path = os.path.join(path, "Logs") # noqa: PTH118 95 | self._optionally_create_directory(path) 96 | return path 97 | 98 | @property 99 | def user_documents_dir(self) -> str: 100 | """:return: documents directory tied to the user e.g. ``%USERPROFILE%\\Documents``""" 101 | return os.path.normpath(get_win_folder("CSIDL_PERSONAL")) 102 | 103 | @property 104 | def user_downloads_dir(self) -> str: 105 | """:return: downloads directory tied to the user e.g. ``%USERPROFILE%\\Downloads``""" 106 | return os.path.normpath(get_win_folder("CSIDL_DOWNLOADS")) 107 | 108 | @property 109 | def user_pictures_dir(self) -> str: 110 | """:return: pictures directory tied to the user e.g. ``%USERPROFILE%\\Pictures``""" 111 | return os.path.normpath(get_win_folder("CSIDL_MYPICTURES")) 112 | 113 | @property 114 | def user_videos_dir(self) -> str: 115 | """:return: videos directory tied to the user e.g. ``%USERPROFILE%\\Videos``""" 116 | return os.path.normpath(get_win_folder("CSIDL_MYVIDEO")) 117 | 118 | @property 119 | def user_music_dir(self) -> str: 120 | """:return: music directory tied to the user e.g. ``%USERPROFILE%\\Music``""" 121 | return os.path.normpath(get_win_folder("CSIDL_MYMUSIC")) 122 | 123 | @property 124 | def user_desktop_dir(self) -> str: 125 | """:return: desktop directory tied to the user, e.g. ``%USERPROFILE%\\Desktop``""" 126 | return os.path.normpath(get_win_folder("CSIDL_DESKTOPDIRECTORY")) 127 | 128 | @property 129 | def user_runtime_dir(self) -> str: 130 | """ 131 | :return: runtime directory tied to the user, e.g. 132 | ``%USERPROFILE%\\AppData\\Local\\Temp\\$appauthor\\$appname`` 133 | """ 134 | path = os.path.normpath(os.path.join(get_win_folder("CSIDL_LOCAL_APPDATA"), "Temp")) # noqa: PTH118 135 | return self._append_parts(path) 136 | 137 | @property 138 | def site_runtime_dir(self) -> str: 139 | """:return: runtime directory shared by users, same as `user_runtime_dir`""" 140 | return self.user_runtime_dir 141 | 142 | 143 | def get_win_folder_from_env_vars(csidl_name: str) -> str: 144 | """Get folder from environment variables.""" 145 | result = get_win_folder_if_csidl_name_not_env_var(csidl_name) 146 | if result is not None: 147 | return result 148 | 149 | env_var_name = { 150 | "CSIDL_APPDATA": "APPDATA", 151 | "CSIDL_COMMON_APPDATA": "ALLUSERSPROFILE", 152 | "CSIDL_LOCAL_APPDATA": "LOCALAPPDATA", 153 | }.get(csidl_name) 154 | if env_var_name is None: 155 | msg = f"Unknown CSIDL name: {csidl_name}" 156 | raise ValueError(msg) 157 | result = os.environ.get(env_var_name) 158 | if result is None: 159 | msg = f"Unset environment variable: {env_var_name}" 160 | raise ValueError(msg) 161 | return result 162 | 163 | 164 | def get_win_folder_if_csidl_name_not_env_var(csidl_name: str) -> str | None: 165 | """Get a folder for a CSIDL name that does not exist as an environment variable.""" 166 | if csidl_name == "CSIDL_PERSONAL": 167 | return os.path.join(os.path.normpath(os.environ["USERPROFILE"]), "Documents") # noqa: PTH118 168 | 169 | if csidl_name == "CSIDL_DOWNLOADS": 170 | return os.path.join(os.path.normpath(os.environ["USERPROFILE"]), "Downloads") # noqa: PTH118 171 | 172 | if csidl_name == "CSIDL_MYPICTURES": 173 | return os.path.join(os.path.normpath(os.environ["USERPROFILE"]), "Pictures") # noqa: PTH118 174 | 175 | if csidl_name == "CSIDL_MYVIDEO": 176 | return os.path.join(os.path.normpath(os.environ["USERPROFILE"]), "Videos") # noqa: PTH118 177 | 178 | if csidl_name == "CSIDL_MYMUSIC": 179 | return os.path.join(os.path.normpath(os.environ["USERPROFILE"]), "Music") # noqa: PTH118 180 | return None 181 | 182 | 183 | def get_win_folder_from_registry(csidl_name: str) -> str: 184 | """ 185 | Get folder from the registry. 186 | 187 | This is a fallback technique at best. I'm not sure if using the registry for these guarantees us the correct answer 188 | for all CSIDL_* names. 189 | 190 | """ 191 | shell_folder_name = { 192 | "CSIDL_APPDATA": "AppData", 193 | "CSIDL_COMMON_APPDATA": "Common AppData", 194 | "CSIDL_LOCAL_APPDATA": "Local AppData", 195 | "CSIDL_PERSONAL": "Personal", 196 | "CSIDL_DOWNLOADS": "{374DE290-123F-4565-9164-39C4925E467B}", 197 | "CSIDL_MYPICTURES": "My Pictures", 198 | "CSIDL_MYVIDEO": "My Video", 199 | "CSIDL_MYMUSIC": "My Music", 200 | }.get(csidl_name) 201 | if shell_folder_name is None: 202 | msg = f"Unknown CSIDL name: {csidl_name}" 203 | raise ValueError(msg) 204 | if sys.platform != "win32": # only needed for mypy type checker to know that this code runs only on Windows 205 | raise NotImplementedError 206 | import winreg # noqa: PLC0415 207 | 208 | key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, r"Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders") 209 | directory, _ = winreg.QueryValueEx(key, shell_folder_name) 210 | return str(directory) 211 | 212 | 213 | def get_win_folder_via_ctypes(csidl_name: str) -> str: 214 | """Get folder with ctypes.""" 215 | # There is no 'CSIDL_DOWNLOADS'. 216 | # Use 'CSIDL_PROFILE' (40) and append the default folder 'Downloads' instead. 217 | # https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid 218 | 219 | import ctypes # noqa: PLC0415 220 | 221 | csidl_const = { 222 | "CSIDL_APPDATA": 26, 223 | "CSIDL_COMMON_APPDATA": 35, 224 | "CSIDL_LOCAL_APPDATA": 28, 225 | "CSIDL_PERSONAL": 5, 226 | "CSIDL_MYPICTURES": 39, 227 | "CSIDL_MYVIDEO": 14, 228 | "CSIDL_MYMUSIC": 13, 229 | "CSIDL_DOWNLOADS": 40, 230 | "CSIDL_DESKTOPDIRECTORY": 16, 231 | }.get(csidl_name) 232 | if csidl_const is None: 233 | msg = f"Unknown CSIDL name: {csidl_name}" 234 | raise ValueError(msg) 235 | 236 | buf = ctypes.create_unicode_buffer(1024) 237 | windll = getattr(ctypes, "windll") # noqa: B009 # using getattr to avoid false positive with mypy type checker 238 | windll.shell32.SHGetFolderPathW(None, csidl_const, None, 0, buf) 239 | 240 | # Downgrade to short path name if it has high-bit chars. 241 | if any(ord(c) > 255 for c in buf): # noqa: PLR2004 242 | buf2 = ctypes.create_unicode_buffer(1024) 243 | if windll.kernel32.GetShortPathNameW(buf.value, buf2, 1024): 244 | buf = buf2 245 | 246 | if csidl_name == "CSIDL_DOWNLOADS": 247 | return os.path.join(buf.value, "Downloads") # noqa: PTH118 248 | 249 | return buf.value 250 | 251 | 252 | def _pick_get_win_folder() -> Callable[[str], str]: 253 | try: 254 | import ctypes # noqa: PLC0415 255 | except ImportError: 256 | pass 257 | else: 258 | if hasattr(ctypes, "windll"): 259 | return get_win_folder_via_ctypes 260 | try: 261 | import winreg # noqa: PLC0415, F401 262 | except ImportError: 263 | return get_win_folder_from_env_vars 264 | else: 265 | return get_win_folder_from_registry 266 | 267 | 268 | get_win_folder = lru_cache(maxsize=None)(_pick_get_win_folder()) 269 | 270 | __all__ = [ 271 | "Windows", 272 | ] 273 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING, cast 4 | 5 | import pytest 6 | 7 | if TYPE_CHECKING: 8 | from _pytest.fixtures import SubRequest 9 | 10 | PROPS = ( 11 | "user_data_dir", 12 | "user_config_dir", 13 | "user_cache_dir", 14 | "user_state_dir", 15 | "user_log_dir", 16 | "user_documents_dir", 17 | "user_downloads_dir", 18 | "user_pictures_dir", 19 | "user_videos_dir", 20 | "user_music_dir", 21 | "user_runtime_dir", 22 | "site_data_dir", 23 | "site_config_dir", 24 | "site_cache_dir", 25 | "site_runtime_dir", 26 | ) 27 | 28 | 29 | @pytest.fixture(params=PROPS) 30 | def func(request: SubRequest) -> str: 31 | return cast("str", request.param) 32 | 33 | 34 | @pytest.fixture(params=PROPS) 35 | def func_path(request: SubRequest) -> str: 36 | prop = cast("str", request.param) 37 | return prop.replace("_dir", "_path") 38 | 39 | 40 | @pytest.fixture 41 | def props() -> tuple[str, ...]: 42 | return PROPS 43 | -------------------------------------------------------------------------------- /tests/test_android.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | from typing import TYPE_CHECKING, Any 5 | from unittest.mock import MagicMock 6 | 7 | import pytest 8 | 9 | from platformdirs.android import Android 10 | 11 | if TYPE_CHECKING: 12 | from pytest_mock import MockerFixture 13 | 14 | 15 | @pytest.mark.parametrize( 16 | "params", 17 | [ 18 | {}, 19 | {"appname": "foo"}, 20 | {"appname": "foo", "appauthor": "bar"}, 21 | {"appname": "foo", "appauthor": "bar", "version": "v1.0"}, 22 | {"appname": "foo", "appauthor": "bar", "version": "v1.0", "opinion": False}, 23 | ], 24 | ids=[ 25 | "no_args", 26 | "app_name", 27 | "app_name_with_app_author", 28 | "app_name_author_version", 29 | "app_name_author_version_false_opinion", 30 | ], 31 | ) 32 | def test_android(mocker: MockerFixture, params: dict[str, Any], func: str) -> None: 33 | mocker.patch("platformdirs.android._android_folder", return_value="/data/data/com.example", autospec=True) 34 | mocker.patch("platformdirs.android.os.path.join", lambda *args: "/".join(args)) 35 | result = getattr(Android(**params), func) 36 | 37 | suffix_elements = [] 38 | if "appname" in params: 39 | suffix_elements.append(params["appname"]) 40 | if "version" in params: 41 | suffix_elements.append(params["version"]) 42 | if suffix_elements: 43 | suffix_elements.insert(0, "") 44 | suffix = "/".join(suffix_elements) 45 | 46 | val = "/tmp" # noqa: S108 47 | expected_map = { 48 | "user_data_dir": f"/data/data/com.example/files{suffix}", 49 | "site_data_dir": f"/data/data/com.example/files{suffix}", 50 | "user_config_dir": f"/data/data/com.example/shared_prefs{suffix}", 51 | "site_config_dir": f"/data/data/com.example/shared_prefs{suffix}", 52 | "user_cache_dir": f"/data/data/com.example/cache{suffix}", 53 | "site_cache_dir": f"/data/data/com.example/cache{suffix}", 54 | "user_state_dir": f"/data/data/com.example/files{suffix}", 55 | "user_log_dir": f"/data/data/com.example/cache{suffix}{'' if params.get('opinion', True) is False else '/log'}", 56 | "user_documents_dir": "/storage/emulated/0/Documents", 57 | "user_downloads_dir": "/storage/emulated/0/Downloads", 58 | "user_pictures_dir": "/storage/emulated/0/Pictures", 59 | "user_videos_dir": "/storage/emulated/0/DCIM/Camera", 60 | "user_music_dir": "/storage/emulated/0/Music", 61 | "user_desktop_dir": "/storage/emulated/0/Desktop", 62 | "user_runtime_dir": f"/data/data/com.example/cache{suffix}{'' if not params.get('opinion', True) else val}", 63 | "site_runtime_dir": f"/data/data/com.example/cache{suffix}{'' if not params.get('opinion', True) else val}", 64 | } 65 | expected = expected_map[func] 66 | 67 | assert result == expected 68 | 69 | 70 | def test_android_folder_from_jnius(mocker: MockerFixture, monkeypatch: pytest.MonkeyPatch) -> None: 71 | from platformdirs import PlatformDirs # noqa: PLC0415 72 | from platformdirs.android import _android_folder # noqa: PLC0415 73 | 74 | mocker.patch.dict(sys.modules, {"android": MagicMock(side_effect=ModuleNotFoundError)}) 75 | monkeypatch.delitem(__import__("sys").modules, "android") 76 | 77 | _android_folder.cache_clear() 78 | 79 | if PlatformDirs is Android: # type: ignore[comparison-overlap] # See https://github.com/platformdirs/platformdirs/pull/295 80 | import jnius # pragma: no cover # noqa: PLC0415 81 | 82 | autoclass = mocker.spy(jnius, "autoclass") # pragma: no cover 83 | else: 84 | parent = MagicMock(return_value=MagicMock(getAbsolutePath=MagicMock(return_value="/A"))) # pragma: no cover 85 | context = MagicMock(getFilesDir=MagicMock(return_value=MagicMock(getParentFile=parent))) # pragma: no cover 86 | autoclass = MagicMock(return_value=context) # pragma: no cover 87 | mocker.patch.dict(sys.modules, {"jnius": MagicMock(autoclass=autoclass)}) # pragma: no cover 88 | 89 | result = _android_folder() 90 | assert result == "/A" 91 | assert autoclass.call_count == 1 92 | 93 | assert autoclass.call_args[0] == ("android.content.Context",) 94 | 95 | assert _android_folder() is result 96 | assert autoclass.call_count == 1 97 | 98 | 99 | def test_android_folder_from_p4a(mocker: MockerFixture, monkeypatch: pytest.MonkeyPatch) -> None: 100 | from platformdirs.android import _android_folder # noqa: PLC0415 101 | 102 | mocker.patch.dict(sys.modules, {"jnius": MagicMock(side_effect=ModuleNotFoundError)}) 103 | monkeypatch.delitem(__import__("sys").modules, "jnius") 104 | 105 | _android_folder.cache_clear() 106 | 107 | get_absolute_path = MagicMock(return_value="/A") 108 | get_parent_file = MagicMock(getAbsolutePath=get_absolute_path) 109 | get_files_dir = MagicMock(getParentFile=MagicMock(return_value=get_parent_file)) 110 | get_application_context = MagicMock(getFilesDir=MagicMock(return_value=get_files_dir)) 111 | m_activity = MagicMock(getApplicationContext=MagicMock(return_value=get_application_context)) 112 | mocker.patch.dict(sys.modules, {"android": MagicMock(mActivity=m_activity)}) 113 | 114 | result = _android_folder() 115 | assert result == "/A" 116 | assert get_absolute_path.call_count == 1 117 | 118 | assert _android_folder() is result 119 | assert get_absolute_path.call_count == 1 120 | 121 | 122 | @pytest.mark.parametrize( 123 | "path", 124 | [ 125 | "/data/user/1/a/files", 126 | "/data/data/a/files", 127 | "/mnt/expand/8e06fc2f-a86a-44e8-81ce-109e0eedd5ed/user/1/a/files", 128 | ], 129 | ) 130 | def test_android_folder_from_sys_path(mocker: MockerFixture, path: str, monkeypatch: pytest.MonkeyPatch) -> None: 131 | mocker.patch.dict(sys.modules, {"jnius": MagicMock(side_effect=ModuleNotFoundError)}) 132 | monkeypatch.delitem(__import__("sys").modules, "jnius") 133 | mocker.patch.dict(sys.modules, {"android": MagicMock(side_effect=ModuleNotFoundError)}) 134 | monkeypatch.delitem(__import__("sys").modules, "android") 135 | 136 | from platformdirs.android import _android_folder # noqa: PLC0415 137 | 138 | _android_folder.cache_clear() 139 | monkeypatch.setattr(sys, "path", ["/A", "/B", path]) 140 | 141 | result = _android_folder() 142 | assert result == path[: -len("/files")] 143 | 144 | 145 | def test_android_folder_not_found(mocker: MockerFixture, monkeypatch: pytest.MonkeyPatch) -> None: 146 | mocker.patch.dict(sys.modules, {"jnius": MagicMock(autoclass=MagicMock(side_effect=ModuleNotFoundError))}) 147 | 148 | from platformdirs.android import _android_folder # noqa: PLC0415 149 | 150 | _android_folder.cache_clear() 151 | monkeypatch.setattr(sys, "path", []) 152 | assert _android_folder() is None 153 | -------------------------------------------------------------------------------- /tests/test_api.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import builtins 4 | import functools 5 | import inspect 6 | import sys 7 | from pathlib import Path 8 | from typing import TYPE_CHECKING, Any, Callable 9 | 10 | import pytest 11 | 12 | import platformdirs 13 | from platformdirs.android import Android 14 | 15 | builtin_import = builtins.__import__ 16 | 17 | 18 | if TYPE_CHECKING: 19 | from types import ModuleType 20 | 21 | 22 | def test_package_metadata() -> None: 23 | assert hasattr(platformdirs, "__version__") 24 | assert hasattr(platformdirs, "__version_info__") 25 | 26 | 27 | def test_method_result_is_str(func: str) -> None: 28 | method = getattr(platformdirs, func) 29 | result = method() 30 | assert isinstance(result, str) 31 | 32 | 33 | def test_property_result_is_str(func: str) -> None: 34 | dirs = platformdirs.PlatformDirs("MyApp", "MyCompany", version="1.0") 35 | result = getattr(dirs, func) 36 | assert isinstance(result, str) 37 | 38 | 39 | def test_method_result_is_path(func_path: str) -> None: 40 | method = getattr(platformdirs, func_path) 41 | result = method() 42 | assert isinstance(result, Path) 43 | 44 | 45 | def test_property_result_is_path(func_path: str) -> None: 46 | dirs = platformdirs.PlatformDirs("MyApp", "MyCompany", version="1.0") 47 | result = getattr(dirs, func_path) 48 | assert isinstance(result, Path) 49 | 50 | 51 | def test_function_interface_is_in_sync(func: str) -> None: 52 | function_dir = getattr(platformdirs, func) 53 | function_path = getattr(platformdirs, func.replace("_dir", "_path")) 54 | assert inspect.isfunction(function_dir) 55 | assert inspect.isfunction(function_path) 56 | function_dir_signature = inspect.Signature.from_callable(function_dir) 57 | function_path_signature = inspect.Signature.from_callable(function_path) 58 | assert function_dir_signature.parameters == function_path_signature.parameters 59 | 60 | 61 | @pytest.mark.parametrize("root", ["A", "/system", None]) 62 | @pytest.mark.parametrize("data", ["D", "/data", None]) 63 | @pytest.mark.parametrize("path", ["/data/data/a/files", "/C"]) 64 | @pytest.mark.parametrize("shell", ["/data/data/com.app/files/usr/bin/sh", "/usr/bin/sh", None]) 65 | @pytest.mark.parametrize("prefix", ["/data/data/com.termux/files/usr", None]) 66 | def test_android_active( # noqa: PLR0913 67 | monkeypatch: pytest.MonkeyPatch, 68 | root: str | None, 69 | data: str | None, 70 | path: str, 71 | shell: str | None, 72 | prefix: str | None, 73 | ) -> None: 74 | for env_var, value in {"ANDROID_DATA": data, "ANDROID_ROOT": root, "SHELL": shell, "PREFIX": prefix}.items(): 75 | if value is None: 76 | monkeypatch.delenv(env_var, raising=False) 77 | else: 78 | monkeypatch.setenv(env_var, value) 79 | 80 | from platformdirs.android import _android_folder # noqa: PLC0415 81 | 82 | _android_folder.cache_clear() 83 | monkeypatch.setattr(sys, "path", ["/A", "/B", path]) 84 | 85 | expected = ( 86 | root == "/system" and data == "/data" and shell is None and prefix is None and _android_folder() is not None 87 | ) 88 | if expected: 89 | assert platformdirs._set_platform_dir_class() is Android # noqa: SLF001 90 | else: 91 | assert platformdirs._set_platform_dir_class() is not Android # noqa: SLF001 92 | 93 | 94 | def _fake_import(name: str, *args: Any, **kwargs: Any) -> ModuleType: # noqa: ANN401 95 | if name == "ctypes": 96 | msg = f"No module named {name}" 97 | raise ModuleNotFoundError(msg) 98 | return builtin_import(name, *args, **kwargs) 99 | 100 | 101 | def mock_import(func: Callable[[], None]) -> Callable[[], None]: 102 | @functools.wraps(func) 103 | def wrap() -> None: 104 | platformdirs_module_items = [item for item in sys.modules.items() if item[0].startswith("platformdirs")] 105 | try: 106 | builtins.__import__ = _fake_import 107 | for name, _ in platformdirs_module_items: 108 | del sys.modules[name] 109 | return func() 110 | finally: 111 | # restore original modules 112 | builtins.__import__ = builtin_import 113 | for name, module in platformdirs_module_items: 114 | sys.modules[name] = module 115 | 116 | return wrap 117 | 118 | 119 | @mock_import 120 | def test_no_ctypes() -> None: 121 | import platformdirs # noqa: PLC0415 122 | 123 | assert platformdirs 124 | 125 | 126 | def test_mypy_subclassing() -> None: 127 | # Ensure that PlatformDirs / AppDirs is seen as a valid superclass by mypy 128 | # This is a static type-checking test to ensure we work around 129 | # the following mypy issue: https://github.com/python/mypy/issues/10962 130 | class PlatformDirsSubclass(platformdirs.PlatformDirs): ... 131 | 132 | class AppDirsSubclass(platformdirs.AppDirs): ... 133 | -------------------------------------------------------------------------------- /tests/test_comp_with_appdirs.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | from inspect import getmembers, isfunction 5 | from typing import Any 6 | 7 | import appdirs 8 | import pytest 9 | 10 | import platformdirs 11 | 12 | 13 | def test_has_backward_compatible_class() -> None: 14 | from platformdirs import AppDirs # noqa: PLC0415 15 | 16 | assert AppDirs is platformdirs.PlatformDirs 17 | 18 | 19 | def test_has_all_functions() -> None: 20 | # Get all public function names from appdirs 21 | appdirs_function_names = [f[0] for f in getmembers(appdirs, isfunction) if not f[0].startswith("_")] 22 | 23 | # Exception will be raised if any appdirs functions aren't in platformdirs. 24 | for function_name in appdirs_function_names: 25 | getattr(platformdirs, function_name) 26 | 27 | 28 | def test_has_all_properties() -> None: 29 | # Get names of all the properties of appdirs.AppDirs 30 | appdirs_property_names = [p[0] for p in getmembers(appdirs.AppDirs, lambda member: isinstance(member, property))] 31 | 32 | # Exception will be raised if any appdirs.AppDirs properties aren't in platformdirs.AppDirs 33 | for property_name in appdirs_property_names: 34 | getattr(platformdirs.AppDirs, property_name) 35 | 36 | 37 | @pytest.mark.parametrize( 38 | "params", 39 | [ 40 | {}, 41 | {"appname": "foo"}, 42 | {"appname": "foo", "appauthor": "bar"}, 43 | {"appname": "foo", "appauthor": "bar", "version": "v1.0"}, 44 | ], 45 | ids=[ 46 | "no_args", 47 | "app_name", 48 | "app_name_with_app_author", 49 | "app_name_author_version", 50 | ], 51 | ) 52 | def test_compatibility(params: dict[str, Any], func: str) -> None: 53 | # Only test functions that are part of appdirs 54 | if getattr(appdirs, func, None) is None: 55 | pytest.skip(f"`{func}` does not exist in `appdirs`") 56 | 57 | if sys.platform == "darwin": 58 | msg = { # pragma: no cover 59 | "user_log_dir": "without appname produces NoneType error", 60 | } 61 | if func in msg: # pragma: no cover 62 | pytest.skip(f"`appdirs.{func}` {msg[func]} on macOS") # pragma: no cover 63 | elif sys.platform != "win32": 64 | msg = { # pragma: no cover 65 | "user_log_dir": "Uses XDG_STATE_DIR instead of appdirs.user_data_dir per the XDG spec", 66 | } 67 | if func in msg: # pragma: no cover 68 | pytest.skip(f"`appdirs.{func}` {msg[func]} on Unix") # pragma: no cover 69 | 70 | new = getattr(platformdirs, func)(*params) 71 | old = getattr(appdirs, func)(*params) 72 | 73 | assert new == old.rstrip("/") 74 | -------------------------------------------------------------------------------- /tests/test_macos.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | import sys 5 | from pathlib import Path 6 | from typing import TYPE_CHECKING, Any 7 | 8 | import pytest 9 | 10 | from platformdirs.macos import MacOS 11 | 12 | if TYPE_CHECKING: 13 | from pytest_mock import MockerFixture 14 | 15 | 16 | @pytest.fixture(autouse=True) 17 | def _fix_os_pathsep(mocker: MockerFixture) -> None: 18 | """If we're not running on macOS, set `os.pathsep` to what it should be on macOS.""" 19 | if sys.platform != "darwin": # pragma: darwin no cover 20 | mocker.patch("os.pathsep", ":") 21 | mocker.patch("os.path.pathsep", ":") 22 | 23 | 24 | @pytest.mark.parametrize( 25 | "params", 26 | [ 27 | pytest.param({}, id="no_args"), 28 | pytest.param({"appname": "foo"}, id="app_name"), 29 | pytest.param({"appname": "foo", "version": "v1.0"}, id="app_name_version"), 30 | ], 31 | ) 32 | def test_macos(mocker: MockerFixture, params: dict[str, Any], func: str) -> None: 33 | # Make sure we are not in Homebrew 34 | py_version = sys.version_info 35 | builtin_py_prefix = ( 36 | "/Applications/Xcode.app/Contents/Developer/Library/Frameworks/Python3.framework" 37 | f"/Versions/{py_version.major}.{py_version.minor}" 38 | ) 39 | mocker.patch("sys.prefix", builtin_py_prefix) 40 | 41 | result = getattr(MacOS(**params), func) 42 | 43 | home = str(Path("~").expanduser()) 44 | suffix_elements = tuple(params[i] for i in ("appname", "version") if i in params) 45 | suffix = os.sep.join(("", *suffix_elements)) if suffix_elements else "" # noqa: PTH118 46 | 47 | expected_map = { 48 | "user_data_dir": f"{home}/Library/Application Support{suffix}", 49 | "site_data_dir": f"/Library/Application Support{suffix}", 50 | "user_config_dir": f"{home}/Library/Application Support{suffix}", 51 | "site_config_dir": f"/Library/Application Support{suffix}", 52 | "user_cache_dir": f"{home}/Library/Caches{suffix}", 53 | "site_cache_dir": f"/Library/Caches{suffix}", 54 | "user_state_dir": f"{home}/Library/Application Support{suffix}", 55 | "user_log_dir": f"{home}/Library/Logs{suffix}", 56 | "user_documents_dir": f"{home}/Documents", 57 | "user_downloads_dir": f"{home}/Downloads", 58 | "user_pictures_dir": f"{home}/Pictures", 59 | "user_videos_dir": f"{home}/Movies", 60 | "user_music_dir": f"{home}/Music", 61 | "user_desktop_dir": f"{home}/Desktop", 62 | "user_runtime_dir": f"{home}/Library/Caches/TemporaryItems{suffix}", 63 | "site_runtime_dir": f"{home}/Library/Caches/TemporaryItems{suffix}", 64 | } 65 | expected = expected_map[func] 66 | 67 | assert result == expected 68 | 69 | 70 | @pytest.mark.parametrize( 71 | "params", 72 | [ 73 | pytest.param({}, id="no_args"), 74 | pytest.param({"appname": "foo"}, id="app_name"), 75 | pytest.param({"appname": "foo", "version": "v1.0"}, id="app_name_version"), 76 | ], 77 | ) 78 | @pytest.mark.parametrize( 79 | "site_func", 80 | [ 81 | "site_data_dir", 82 | "site_config_dir", 83 | "site_cache_dir", 84 | "site_runtime_dir", 85 | "site_cache_path", 86 | "site_data_path", 87 | ], 88 | ) 89 | @pytest.mark.parametrize("multipath", [pytest.param(True, id="multipath"), pytest.param(False, id="singlepath")]) 90 | def test_macos_homebrew(mocker: MockerFixture, params: dict[str, Any], multipath: bool, site_func: str) -> None: 91 | mocker.patch("sys.prefix", "/opt/homebrew/opt/python") 92 | 93 | result = getattr(MacOS(multipath=multipath, **params), site_func) 94 | 95 | home = str(Path("~").expanduser()) 96 | suffix_elements = tuple(params[i] for i in ("appname", "version") if i in params) 97 | suffix = os.sep.join(("", *suffix_elements)) if suffix_elements else "" # noqa: PTH118 98 | 99 | expected_path_map = { 100 | "site_cache_path": Path(f"/opt/homebrew/var/cache{suffix}"), 101 | "site_data_path": Path(f"/opt/homebrew/share{suffix}"), 102 | } 103 | expected_map = { 104 | "site_data_dir": f"/opt/homebrew/share{suffix}", 105 | "site_config_dir": f"/opt/homebrew/share{suffix}", 106 | "site_cache_dir": f"/opt/homebrew/var/cache{suffix}", 107 | "site_runtime_dir": f"{home}/Library/Caches/TemporaryItems{suffix}", 108 | } 109 | if multipath: 110 | expected_map["site_data_dir"] += f":/Library/Application Support{suffix}" 111 | expected_map["site_config_dir"] += f":/Library/Application Support{suffix}" 112 | expected_map["site_cache_dir"] += f":/Library/Caches{suffix}" 113 | expected = expected_path_map[site_func] if site_func.endswith("_path") else expected_map[site_func] 114 | 115 | assert result == expected 116 | -------------------------------------------------------------------------------- /tests/test_main.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | from subprocess import check_output # noqa: S404 5 | 6 | from platformdirs import __version__ 7 | from platformdirs.__main__ import PROPS 8 | 9 | 10 | def test_props_same_as_test(props: tuple[str, ...]) -> None: 11 | assert props == PROPS 12 | 13 | 14 | def test_run_as_module() -> None: 15 | out = check_output([sys.executable, "-m", "platformdirs"], text=True) 16 | 17 | assert out.startswith(f"-- platformdirs {__version__} --") 18 | for prop in PROPS: 19 | assert prop in out 20 | -------------------------------------------------------------------------------- /tests/test_unix.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import importlib 4 | import os 5 | import sys 6 | import typing 7 | 8 | import pytest 9 | 10 | from platformdirs import unix 11 | from platformdirs.unix import Unix 12 | 13 | if typing.TYPE_CHECKING: 14 | from pathlib import Path 15 | 16 | from pytest_mock import MockerFixture 17 | 18 | 19 | @pytest.mark.parametrize( 20 | "prop", 21 | [ 22 | "user_documents_dir", 23 | "user_downloads_dir", 24 | "user_pictures_dir", 25 | "user_videos_dir", 26 | "user_music_dir", 27 | "user_desktop_dir", 28 | ], 29 | ) 30 | def test_user_media_dir(mocker: MockerFixture, prop: str) -> None: 31 | example_path = "/home/example/ExampleMediaFolder" 32 | mock = mocker.patch("platformdirs.unix._get_user_dirs_folder") 33 | mock.return_value = example_path 34 | assert getattr(Unix(), prop) == example_path 35 | 36 | 37 | @pytest.mark.parametrize( 38 | ("env_var", "prop"), 39 | [ 40 | pytest.param("XDG_DOCUMENTS_DIR", "user_documents_dir", id="user_documents_dir"), 41 | pytest.param("XDG_DOWNLOAD_DIR", "user_downloads_dir", id="user_downloads_dir"), 42 | pytest.param("XDG_PICTURES_DIR", "user_pictures_dir", id="user_pictures_dir"), 43 | pytest.param("XDG_VIDEOS_DIR", "user_videos_dir", id="user_videos_dir"), 44 | pytest.param("XDG_MUSIC_DIR", "user_music_dir", id="user_music_dir"), 45 | pytest.param("XDG_DESKTOP_DIR", "user_desktop_dir", id="user_desktop_dir"), 46 | ], 47 | ) 48 | def test_user_media_dir_env_var(mocker: MockerFixture, env_var: str, prop: str) -> None: 49 | # Mock media dir not being in user-dirs.dirs file 50 | mock = mocker.patch("platformdirs.unix._get_user_dirs_folder") 51 | mock.return_value = None 52 | 53 | example_path = "/home/example/ExampleMediaFolder" 54 | mocker.patch.dict(os.environ, {env_var: example_path}) 55 | 56 | assert getattr(Unix(), prop) == example_path 57 | 58 | 59 | @pytest.mark.parametrize( 60 | ("env_var", "prop", "default_abs_path"), 61 | [ 62 | pytest.param("XDG_DOCUMENTS_DIR", "user_documents_dir", "/home/example/Documents", id="user_documents_dir"), 63 | pytest.param("XDG_DOWNLOAD_DIR", "user_downloads_dir", "/home/example/Downloads", id="user_downloads_dir"), 64 | pytest.param("XDG_PICTURES_DIR", "user_pictures_dir", "/home/example/Pictures", id="user_pictures_dir"), 65 | pytest.param("XDG_VIDEOS_DIR", "user_videos_dir", "/home/example/Videos", id="user_videos_dir"), 66 | pytest.param("XDG_MUSIC_DIR", "user_music_dir", "/home/example/Music", id="user_music_dir"), 67 | pytest.param("XDG_DESKTOP_DIR", "user_desktop_dir", "/home/example/Desktop", id="user_desktop_dir"), 68 | ], 69 | ) 70 | def test_user_media_dir_default(mocker: MockerFixture, env_var: str, prop: str, default_abs_path: str) -> None: 71 | # Mock media dir not being in user-dirs.dirs file 72 | mock = mocker.patch("platformdirs.unix._get_user_dirs_folder") 73 | mock.return_value = None 74 | 75 | # Mock no XDG env variable being set 76 | mocker.patch.dict(os.environ, {env_var: ""}) 77 | 78 | # Mock home directory 79 | mocker.patch.dict(os.environ, {"HOME": "/home/example"}) 80 | # Mock home directory for running the test on Windows 81 | mocker.patch.dict(os.environ, {"USERPROFILE": "/home/example"}) 82 | 83 | assert getattr(Unix(), prop) == default_abs_path 84 | 85 | 86 | class XDGVariable(typing.NamedTuple): 87 | name: str 88 | default_value: str 89 | 90 | 91 | def _func_to_path(func: str) -> XDGVariable | None: 92 | mapping = { 93 | "user_data_dir": XDGVariable("XDG_DATA_HOME", "~/.local/share"), 94 | "site_data_dir": XDGVariable("XDG_DATA_DIRS", f"/usr/local/share{os.pathsep}/usr/share"), 95 | "user_config_dir": XDGVariable("XDG_CONFIG_HOME", "~/.config"), 96 | "site_config_dir": XDGVariable("XDG_CONFIG_DIRS", "/etc/xdg"), 97 | "user_cache_dir": XDGVariable("XDG_CACHE_HOME", "~/.cache"), 98 | "user_state_dir": XDGVariable("XDG_STATE_HOME", "~/.local/state"), 99 | "user_log_dir": XDGVariable("XDG_STATE_HOME", "~/.local/state"), 100 | "user_runtime_dir": XDGVariable("XDG_RUNTIME_DIR", "/run/user/1234"), 101 | "site_runtime_dir": XDGVariable("XDG_RUNTIME_DIR", "/run"), 102 | } 103 | return mapping.get(func) 104 | 105 | 106 | @pytest.fixture 107 | def dirs_instance() -> Unix: 108 | return Unix(multipath=True, opinion=False) 109 | 110 | 111 | @pytest.fixture 112 | def _getuid(mocker: MockerFixture) -> None: 113 | mocker.patch("platformdirs.unix.getuid", return_value=1234) 114 | 115 | 116 | @pytest.mark.usefixtures("_getuid") 117 | def test_xdg_variable_not_set(monkeypatch: pytest.MonkeyPatch, dirs_instance: Unix, func: str) -> None: 118 | xdg_variable = _func_to_path(func) 119 | if xdg_variable is None: 120 | return 121 | 122 | monkeypatch.delenv(xdg_variable.name, raising=False) 123 | result = getattr(dirs_instance, func) 124 | assert result == os.path.expanduser(xdg_variable.default_value) # noqa: PTH111 125 | 126 | 127 | @pytest.mark.usefixtures("_getuid") 128 | def test_xdg_variable_empty_value(monkeypatch: pytest.MonkeyPatch, dirs_instance: Unix, func: str) -> None: 129 | xdg_variable = _func_to_path(func) 130 | if xdg_variable is None: 131 | return 132 | 133 | monkeypatch.setenv(xdg_variable.name, "") 134 | result = getattr(dirs_instance, func) 135 | assert result == os.path.expanduser(xdg_variable.default_value) # noqa: PTH111 136 | 137 | 138 | @pytest.mark.usefixtures("_getuid") 139 | def test_xdg_variable_custom_value(monkeypatch: pytest.MonkeyPatch, dirs_instance: Unix, func: str) -> None: 140 | xdg_variable = _func_to_path(func) 141 | if xdg_variable is None: 142 | return 143 | 144 | monkeypatch.setenv(xdg_variable.name, "/custom-dir") 145 | result = getattr(dirs_instance, func) 146 | assert result == "/custom-dir" 147 | 148 | 149 | @pytest.mark.usefixtures("_getuid") 150 | @pytest.mark.parametrize("platform", ["freebsd", "openbsd", "netbsd"]) 151 | def test_platform_on_bsd(monkeypatch: pytest.MonkeyPatch, mocker: MockerFixture, platform: str) -> None: 152 | monkeypatch.delenv("XDG_RUNTIME_DIR", raising=False) 153 | mocker.patch("sys.platform", platform) 154 | 155 | assert Unix().site_runtime_dir == "/var/run" 156 | 157 | mocker.patch("pathlib.Path.exists", return_value=True) 158 | assert Unix().user_runtime_dir == "/var/run/user/1234" 159 | 160 | mocker.patch("pathlib.Path.exists", return_value=False) 161 | assert Unix().user_runtime_dir == "/tmp/runtime-1234" # noqa: S108 162 | 163 | 164 | def test_platform_on_win32(monkeypatch: pytest.MonkeyPatch, mocker: MockerFixture) -> None: 165 | monkeypatch.delenv("XDG_RUNTIME_DIR", raising=False) 166 | mocker.patch("sys.platform", "win32") 167 | prev_unix = unix 168 | importlib.reload(unix) 169 | try: 170 | with pytest.raises(RuntimeError, match="should only be used on Unix"): 171 | unix.Unix().user_runtime_dir # noqa: B018 172 | finally: 173 | sys.modules["platformdirs.unix"] = prev_unix 174 | 175 | 176 | def test_ensure_exists_creates_folder(mocker: MockerFixture, tmp_path: Path) -> None: 177 | mocker.patch.dict(os.environ, {"XDG_DATA_HOME": str(tmp_path)}) 178 | data_path = Unix(appname="acme", ensure_exists=True).user_data_path 179 | assert data_path.exists() 180 | 181 | 182 | def test_folder_not_created_without_ensure_exists(mocker: MockerFixture, tmp_path: Path) -> None: 183 | mocker.patch.dict(os.environ, {"XDG_DATA_HOME": str(tmp_path)}) 184 | data_path = Unix(appname="acme", ensure_exists=False).user_data_path 185 | assert not data_path.exists() 186 | --------------------------------------------------------------------------------