diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..c9e4267 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @srstevenson diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..097dc26 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,80 @@ +# Contributing + +Thanks for considering contributing! The following is a set of guidelines for +doing so. They're guidelines rather than rules, so follow your best judgement, +but reading them will help make the contribution process easier and more +effective for both you and the maintainers. + +## Reporting issues + +GitHub issues are used for managing bug reports and feature requests, except +security vulnerabilities: these should be emailed to the maintainers instead. + +Search for existing issues before creating a new one, to ensure your problem +hasn't already been reported. If it has, you're welcome to comment on the +existing issue with extra information that might help reproduce and fix the +problem, or sharing why a feature would be useful, but refrain from "+1" type +comments. Duplicate issues will be closed with a reference to the existing +issue. + +In your report describe what you did, what you expected to happen, and what +happened instead. Provide a [minimal reproducible example][mre] that the +maintainers can run. Provide as much detail as you can in your description of +the problem, including the version of the project you're using, and details of +your operating system and environment, and other information which might help +diagnose the problem, such as what you've already tried to fix it. + +## Contributing changes + +### Planning + +When you contribute a new change, the responsibility for maintenance is (by +default) transferred to the existing project maintainers. The benefit of the +contribution must be weighed against the cost of maintaining it. + +If you're considering contributing a non-trivial bugfix or feature, discuss the +changes you plan to make before you start coding by opening an issue. This +ensures your proposed change will be accepted, and provides the maintainers the +opportunity to help you. + +### Implementation + +Changes are managed using GitHub pull requests. If you're new to pull requests, +read the [documentation][pr docs] to learn how they work. + +[Poetry][poetry] is used for managing dependencies and packaging, and you will +need it installed. If you're not familiar with Poetry, we suggest reading its +documentation before you begin. + +After cloning the repository, you can implement your changes as follows: + +1. Install the project and its dependencies into an isolated virtual environment + with `poetry install`. +2. Before making your changes, run the linters and test suite with + `poetry run poe check`, and ensure they pass. This checks your development + environment is correctly configured, and there aren't outstanding issues + before you start coding. If they don't pass, you can open a GitHub issue for + help debugging. +3. Checkout a new branch for your changes, branching from `main`, with a + sensible name for your changes. +4. Implement your changes. +5. If you introduced new functionality or fixed a bug, add appropriate automated + tests to prevent future regressions. +6. Ensure you've updated any docstrings or documentation files (including + `README.md`) which are affected by your change. +7. Run the linters and test suite again with `poetry run poe check`, and fix any + problems. +8. Commit your changes, following [these guidelines][commit guidelines] for your + commit messages. +9. Fork the base repository on GitHub, push your branch to your fork, and open a + pull request against the base repository. Make sure your pull request has a + clear title and description. The easier your changes are to understand, the + easier it is for the maintainers to approve and merge them. +10. Your pull request will be reviewed by the maintainers and either merged, or + feedback will be provided on changes that are required. + +[commit guidelines]: + https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html +[mre]: https://stackoverflow.com/help/minimal-reproducible-example +[poetry]: https://python-poetry.org/ +[pr docs]: https://docs.github.com/en/github/collaborating-with-pull-requests diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..b8b8480 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "monthly" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..c661d35 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,39 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +jobs: + checks: + name: Run checks + runs-on: ubuntu-latest + strategy: + matrix: + python: + - "3.10" + - "3.11" + - "3.12" + - "3.13" + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python }} + + - name: Install dependencies + run: | + python3 -m pip install coverage poetry + poetry install + + - name: Run checks + run: poetry run poe check + + - name: Create XML coverage report + run: poetry run coverage xml + + - name: Upload test coverage report + uses: codecov/codecov-action@v4.6.0 diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 0000000..e40d98e --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,26 @@ +name: Close stale issues and PRs + +on: + schedule: + - cron: "0 3 * * *" + +permissions: + issues: write + pull-requests: write + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v9 + with: + stale-issue-message: "" + stale-pr-message: "" + stale-issue-label: stale + stale-pr-label: stale + close-issue-message: >- + This issue was closed due to inactivity. Please reopen if still + relevant. + close-pr-message: >- + This PR was closed due to inactivity. Please reopen if still + relevant. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c3c95a7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +*.egg-info +.mypy_cache/ +/.coverage +/build/ +/coverage.xml +/dist/ +__pycache__/ diff --git a/.prettierrc.toml b/.prettierrc.toml new file mode 100644 index 0000000..a9c0555 --- /dev/null +++ b/.prettierrc.toml @@ -0,0 +1 @@ +proseWrap = "always" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0af401d --- /dev/null +++ b/LICENSE @@ -0,0 +1,13 @@ +Copyright © Scott Stevenson + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..01a21df --- /dev/null +++ b/README.md @@ -0,0 +1,74 @@ +# xdg-base-dirs + +`xdg-base-dirs` is a Python module that provides functions to return paths to +the directories defined by the [XDG Base Directory Specification][spec], to save +you from duplicating the same snippet of logic in every Python utility you write +that deals with user cache, configuration, or data files. It has no external +dependencies. + +`xdg-base-dirs` currently implements version 0.8 of the specification, released +on 8th May 2021. + +> [!NOTE] +> +> `xdg-base-dirs` was previously named `xdg`, and was renamed due to an import +> collision with [`PyXDG`](https://pypi.org/project/pyxdg/). If you used `xdg` +> prior to the rename, update by changing the dependency name from `xdg` to +> `xdg-base-dirs` and the import from `xdg` to `xdg_base_dirs`. + +## Installation + +`xdg-base-dirs` requires Python 3.10 or later. To install the latest release +from [PyPI] with [pip], use: + +```bash +python3 -m pip install xdg-base-dirs +``` + +Alternatively, since `xdg-base-dirs` is only a single file you may prefer to +just copy `src/xdg_base_dirs/__init__.py` from the source distribution into your +project. + +## Usage + +```python +from xdg_base_dirs import ( + xdg_cache_home, + xdg_config_dirs, + xdg_config_home, + xdg_data_dirs, + xdg_data_home, + xdg_runtime_dir, + xdg_state_home, +) +``` + +`xdg_cache_home()`, `xdg_config_home()`, `xdg_data_home()`, and +`xdg_state_home()` return [`pathlib.Path` objects][path] containing the value of +the environment variable named `XDG_CACHE_HOME`, `XDG_CONFIG_HOME`, +`XDG_DATA_HOME`, and `XDG_STATE_HOME` respectively, or the default defined in +the specification if the environment variable is unset, empty, or contains a +relative path rather than absolute path. + +`xdg_config_dirs()` and `xdg_data_dirs()` return a list of `pathlib.Path` +objects containing the value, split on colons, of the environment variable named +`XDG_CONFIG_DIRS` and `XDG_DATA_DIRS` respectively, or the default defined in +the specification if the environment variable is unset or empty. Relative paths +are ignored, as per the specification. + +`xdg_runtime_dir()` returns a `pathlib.Path` object containing the value of the +`XDG_RUNTIME_DIR` environment variable, or `None` if the environment variable is +not set, or contains a relative path rather than an absolute path. + +## Copyright + +Copyright © Scott Stevenson. + +`xdg-base-dirs` is distributed under the terms of the [ISC license]. + +[isc license]: https://opensource.org/licenses/ISC +[path]: https://docs.python.org/3/library/pathlib.html#pathlib.Path +[pip]: https://pip.pypa.io/en/stable/ +[pypi]: https://pypi.org/project/xdg-base-dirs/ +[spec]: + https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..9905815 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,374 @@ +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "coverage" +version = "7.3.2" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "coverage-7.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d872145f3a3231a5f20fd48500274d7df222e291d90baa2026cc5152b7ce86bf"}, + {file = "coverage-7.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:310b3bb9c91ea66d59c53fa4989f57d2436e08f18fb2f421a1b0b6b8cc7fffda"}, + {file = "coverage-7.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f47d39359e2c3779c5331fc740cf4bce6d9d680a7b4b4ead97056a0ae07cb49a"}, + {file = "coverage-7.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa72dbaf2c2068404b9870d93436e6d23addd8bbe9295f49cbca83f6e278179c"}, + {file = "coverage-7.3.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:beaa5c1b4777f03fc63dfd2a6bd820f73f036bfb10e925fce067b00a340d0f3f"}, + {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:dbc1b46b92186cc8074fee9d9fbb97a9dd06c6cbbef391c2f59d80eabdf0faa6"}, + {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:315a989e861031334d7bee1f9113c8770472db2ac484e5b8c3173428360a9148"}, + {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d1bc430677773397f64a5c88cb522ea43175ff16f8bfcc89d467d974cb2274f9"}, + {file = "coverage-7.3.2-cp310-cp310-win32.whl", hash = "sha256:a889ae02f43aa45032afe364c8ae84ad3c54828c2faa44f3bfcafecb5c96b02f"}, + {file = "coverage-7.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c0ba320de3fb8c6ec16e0be17ee1d3d69adcda99406c43c0409cb5c41788a611"}, + {file = "coverage-7.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ac8c802fa29843a72d32ec56d0ca792ad15a302b28ca6203389afe21f8fa062c"}, + {file = "coverage-7.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:89a937174104339e3a3ffcf9f446c00e3a806c28b1841c63edb2b369310fd074"}, + {file = "coverage-7.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e267e9e2b574a176ddb983399dec325a80dbe161f1a32715c780b5d14b5f583a"}, + {file = "coverage-7.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2443cbda35df0d35dcfb9bf8f3c02c57c1d6111169e3c85fc1fcc05e0c9f39a3"}, + {file = "coverage-7.3.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4175e10cc8dda0265653e8714b3174430b07c1dca8957f4966cbd6c2b1b8065a"}, + {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0cbf38419fb1a347aaf63481c00f0bdc86889d9fbf3f25109cf96c26b403fda1"}, + {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5c913b556a116b8d5f6ef834038ba983834d887d82187c8f73dec21049abd65c"}, + {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1981f785239e4e39e6444c63a98da3a1db8e971cb9ceb50a945ba6296b43f312"}, + {file = "coverage-7.3.2-cp311-cp311-win32.whl", hash = "sha256:43668cabd5ca8258f5954f27a3aaf78757e6acf13c17604d89648ecc0cc66640"}, + {file = "coverage-7.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10c39c0452bf6e694511c901426d6b5ac005acc0f78ff265dbe36bf81f808a2"}, + {file = "coverage-7.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4cbae1051ab791debecc4a5dcc4a1ff45fc27b91b9aee165c8a27514dd160836"}, + {file = "coverage-7.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12d15ab5833a997716d76f2ac1e4b4d536814fc213c85ca72756c19e5a6b3d63"}, + {file = "coverage-7.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c7bba973ebee5e56fe9251300c00f1579652587a9f4a5ed8404b15a0471f216"}, + {file = "coverage-7.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe494faa90ce6381770746077243231e0b83ff3f17069d748f645617cefe19d4"}, + {file = "coverage-7.3.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6e9589bd04d0461a417562649522575d8752904d35c12907d8c9dfeba588faf"}, + {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d51ac2a26f71da1b57f2dc81d0e108b6ab177e7d30e774db90675467c847bbdf"}, + {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:99b89d9f76070237975b315b3d5f4d6956ae354a4c92ac2388a5695516e47c84"}, + {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fa28e909776dc69efb6ed975a63691bc8172b64ff357e663a1bb06ff3c9b589a"}, + {file = "coverage-7.3.2-cp312-cp312-win32.whl", hash = "sha256:289fe43bf45a575e3ab10b26d7b6f2ddb9ee2dba447499f5401cfb5ecb8196bb"}, + {file = "coverage-7.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:7dbc3ed60e8659bc59b6b304b43ff9c3ed858da2839c78b804973f613d3e92ed"}, + {file = "coverage-7.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f94b734214ea6a36fe16e96a70d941af80ff3bfd716c141300d95ebc85339738"}, + {file = "coverage-7.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:af3d828d2c1cbae52d34bdbb22fcd94d1ce715d95f1a012354a75e5913f1bda2"}, + {file = "coverage-7.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:630b13e3036e13c7adc480ca42fa7afc2a5d938081d28e20903cf7fd687872e2"}, + {file = "coverage-7.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9eacf273e885b02a0273bb3a2170f30e2d53a6d53b72dbe02d6701b5296101c"}, + {file = "coverage-7.3.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8f17966e861ff97305e0801134e69db33b143bbfb36436efb9cfff6ec7b2fd9"}, + {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b4275802d16882cf9c8b3d057a0839acb07ee9379fa2749eca54efbce1535b82"}, + {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:72c0cfa5250f483181e677ebc97133ea1ab3eb68645e494775deb6a7f6f83901"}, + {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:cb536f0dcd14149425996821a168f6e269d7dcd2c273a8bff8201e79f5104e76"}, + {file = "coverage-7.3.2-cp38-cp38-win32.whl", hash = "sha256:307adb8bd3abe389a471e649038a71b4eb13bfd6b7dd9a129fa856f5c695cf92"}, + {file = "coverage-7.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:88ed2c30a49ea81ea3b7f172e0269c182a44c236eb394718f976239892c0a27a"}, + {file = "coverage-7.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b631c92dfe601adf8f5ebc7fc13ced6bb6e9609b19d9a8cd59fa47c4186ad1ce"}, + {file = "coverage-7.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d3d9df4051c4a7d13036524b66ecf7a7537d14c18a384043f30a303b146164e9"}, + {file = "coverage-7.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f7363d3b6a1119ef05015959ca24a9afc0ea8a02c687fe7e2d557705375c01f"}, + {file = "coverage-7.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2f11cc3c967a09d3695d2a6f03fb3e6236622b93be7a4b5dc09166a861be6d25"}, + {file = "coverage-7.3.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:149de1d2401ae4655c436a3dced6dd153f4c3309f599c3d4bd97ab172eaf02d9"}, + {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3a4006916aa6fee7cd38db3bfc95aa9c54ebb4ffbfc47c677c8bba949ceba0a6"}, + {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9028a3871280110d6e1aa2df1afd5ef003bab5fb1ef421d6dc748ae1c8ef2ebc"}, + {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9f805d62aec8eb92bab5b61c0f07329275b6f41c97d80e847b03eb894f38d083"}, + {file = "coverage-7.3.2-cp39-cp39-win32.whl", hash = "sha256:d1c88ec1a7ff4ebca0219f5b1ef863451d828cccf889c173e1253aa84b1e07ce"}, + {file = "coverage-7.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b4767da59464bb593c07afceaddea61b154136300881844768037fd5e859353f"}, + {file = "coverage-7.3.2-pp38.pp39.pp310-none-any.whl", hash = "sha256:ae97af89f0fbf373400970c0a21eef5aa941ffeed90aee43650b81f7d7f47637"}, + {file = "coverage-7.3.2.tar.gz", hash = "sha256:be32ad29341b0170e795ca590e1c07e81fc061cb5b10c74ce7203491484404ef"}, +] + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} + +[package.extras] +toml = ["tomli"] + +[[package]] +name = "exceptiongroup" +version = "1.1.3" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.1.3-py3-none-any.whl", hash = "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"}, + {file = "exceptiongroup-1.1.3.tar.gz", hash = "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "mypy" +version = "1.11.2" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mypy-1.11.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d42a6dd818ffce7be66cce644f1dff482f1d97c53ca70908dff0b9ddc120b77a"}, + {file = "mypy-1.11.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:801780c56d1cdb896eacd5619a83e427ce436d86a3bdf9112527f24a66618fef"}, + {file = "mypy-1.11.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41ea707d036a5307ac674ea172875f40c9d55c5394f888b168033177fce47383"}, + {file = "mypy-1.11.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6e658bd2d20565ea86da7d91331b0eed6d2eee22dc031579e6297f3e12c758c8"}, + {file = "mypy-1.11.2-cp310-cp310-win_amd64.whl", hash = "sha256:478db5f5036817fe45adb7332d927daa62417159d49783041338921dcf646fc7"}, + {file = "mypy-1.11.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:75746e06d5fa1e91bfd5432448d00d34593b52e7e91a187d981d08d1f33d4385"}, + {file = "mypy-1.11.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a976775ab2256aadc6add633d44f100a2517d2388906ec4f13231fafbb0eccca"}, + {file = "mypy-1.11.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd953f221ac1379050a8a646585a29574488974f79d8082cedef62744f0a0104"}, + {file = "mypy-1.11.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:57555a7715c0a34421013144a33d280e73c08df70f3a18a552938587ce9274f4"}, + {file = "mypy-1.11.2-cp311-cp311-win_amd64.whl", hash = "sha256:36383a4fcbad95f2657642a07ba22ff797de26277158f1cc7bd234821468b1b6"}, + {file = "mypy-1.11.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e8960dbbbf36906c5c0b7f4fbf2f0c7ffb20f4898e6a879fcf56a41a08b0d318"}, + {file = "mypy-1.11.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:06d26c277962f3fb50e13044674aa10553981ae514288cb7d0a738f495550b36"}, + {file = "mypy-1.11.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e7184632d89d677973a14d00ae4d03214c8bc301ceefcdaf5c474866814c987"}, + {file = "mypy-1.11.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3a66169b92452f72117e2da3a576087025449018afc2d8e9bfe5ffab865709ca"}, + {file = "mypy-1.11.2-cp312-cp312-win_amd64.whl", hash = "sha256:969ea3ef09617aff826885a22ece0ddef69d95852cdad2f60c8bb06bf1f71f70"}, + {file = "mypy-1.11.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:37c7fa6121c1cdfcaac97ce3d3b5588e847aa79b580c1e922bb5d5d2902df19b"}, + {file = "mypy-1.11.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4a8a53bc3ffbd161b5b2a4fff2f0f1e23a33b0168f1c0778ec70e1a3d66deb86"}, + {file = "mypy-1.11.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ff93107f01968ed834f4256bc1fc4475e2fecf6c661260066a985b52741ddce"}, + {file = "mypy-1.11.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:edb91dded4df17eae4537668b23f0ff6baf3707683734b6a818d5b9d0c0c31a1"}, + {file = "mypy-1.11.2-cp38-cp38-win_amd64.whl", hash = "sha256:ee23de8530d99b6db0573c4ef4bd8f39a2a6f9b60655bf7a1357e585a3486f2b"}, + {file = "mypy-1.11.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:801ca29f43d5acce85f8e999b1e431fb479cb02d0e11deb7d2abb56bdaf24fd6"}, + {file = "mypy-1.11.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:af8d155170fcf87a2afb55b35dc1a0ac21df4431e7d96717621962e4b9192e70"}, + {file = "mypy-1.11.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7821776e5c4286b6a13138cc935e2e9b6fde05e081bdebf5cdb2bb97c9df81d"}, + {file = "mypy-1.11.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:539c570477a96a4e6fb718b8d5c3e0c0eba1f485df13f86d2970c91f0673148d"}, + {file = "mypy-1.11.2-cp39-cp39-win_amd64.whl", hash = "sha256:3f14cd3d386ac4d05c5a39a51b84387403dadbd936e17cb35882134d4f8f0d24"}, + {file = "mypy-1.11.2-py3-none-any.whl", hash = "sha256:b499bc07dbdcd3de92b0a8b29fdf592c111276f6a12fe29c30f6c417dd546d12"}, + {file = "mypy-1.11.2.tar.gz", hash = "sha256:7f9993ad3e0ffdc95c2a14b66dee63729f021968bff8ad911867579c65d13a79"}, +] + +[package.dependencies] +mypy-extensions = ">=1.0.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = ">=4.6.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "packaging" +version = "23.2" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, + {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, +] + +[[package]] +name = "pastel" +version = "0.2.1" +description = "Bring colors to your terminal." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "pastel-0.2.1-py2.py3-none-any.whl", hash = "sha256:4349225fcdf6c2bb34d483e523475de5bb04a5c10ef711263452cb37d7dd4364"}, + {file = "pastel-0.2.1.tar.gz", hash = "sha256:e6581ac04e973cac858828c6202c1e1e81fee1dc7de7683f3e1ffe0bfd8a573d"}, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "poethepoet" +version = "0.29.0" +description = "A task runner that works well with poetry." +optional = false +python-versions = ">=3.8" +files = [ + {file = "poethepoet-0.29.0-py3-none-any.whl", hash = "sha256:f8dfe55006dcfb5cf31bcb1904e1262e1c642a4502fee3688cbf1bddfe5c7601"}, + {file = "poethepoet-0.29.0.tar.gz", hash = "sha256:676842302f2304a86b31ac56398dd672fae8471128d2086896393384dbafc095"}, +] + +[package.dependencies] +pastel = ">=0.2.1,<0.3.0" +pyyaml = ">=6.0.2,<7.0.0" +tomli = {version = ">=1.2.2", markers = "python_version < \"3.11\""} + +[package.extras] +poetry-plugin = ["poetry (>=1.0,<2.0)"] + +[[package]] +name = "pytest" +version = "8.3.3" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"}, + {file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.5,<2" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-cov" +version = "5.0.0" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"}, + {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, +] + +[package.dependencies] +coverage = {version = ">=5.2.1", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] + +[[package]] +name = "pyyaml" +version = "6.0.2" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, + {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, + {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, + {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, + {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, + {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, + {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, + {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, + {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, + {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, + {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, + {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, + {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, + {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, + {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, + {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, +] + +[[package]] +name = "ruff" +version = "0.7.0" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruff-0.7.0-py3-none-linux_armv6l.whl", hash = "sha256:0cdf20c2b6ff98e37df47b2b0bd3a34aaa155f59a11182c1303cce79be715628"}, + {file = "ruff-0.7.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:496494d350c7fdeb36ca4ef1c9f21d80d182423718782222c29b3e72b3512737"}, + {file = "ruff-0.7.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:214b88498684e20b6b2b8852c01d50f0651f3cc6118dfa113b4def9f14faaf06"}, + {file = "ruff-0.7.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:630fce3fefe9844e91ea5bbf7ceadab4f9981f42b704fae011bb8efcaf5d84be"}, + {file = "ruff-0.7.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:211d877674e9373d4bb0f1c80f97a0201c61bcd1e9d045b6e9726adc42c156aa"}, + {file = "ruff-0.7.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:194d6c46c98c73949a106425ed40a576f52291c12bc21399eb8f13a0f7073495"}, + {file = "ruff-0.7.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:82c2579b82b9973a110fab281860403b397c08c403de92de19568f32f7178598"}, + {file = "ruff-0.7.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9af971fe85dcd5eaed8f585ddbc6bdbe8c217fb8fcf510ea6bca5bdfff56040e"}, + {file = "ruff-0.7.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b641c7f16939b7d24b7bfc0be4102c56562a18281f84f635604e8a6989948914"}, + {file = "ruff-0.7.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d71672336e46b34e0c90a790afeac8a31954fd42872c1f6adaea1dff76fd44f9"}, + {file = "ruff-0.7.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ab7d98c7eed355166f367597e513a6c82408df4181a937628dbec79abb2a1fe4"}, + {file = "ruff-0.7.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1eb54986f770f49edb14f71d33312d79e00e629a57387382200b1ef12d6a4ef9"}, + {file = "ruff-0.7.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:dc452ba6f2bb9cf8726a84aa877061a2462afe9ae0ea1d411c53d226661c601d"}, + {file = "ruff-0.7.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:4b406c2dce5be9bad59f2de26139a86017a517e6bcd2688da515481c05a2cb11"}, + {file = "ruff-0.7.0-py3-none-win32.whl", hash = "sha256:f6c968509f767776f524a8430426539587d5ec5c662f6addb6aa25bc2e8195ec"}, + {file = "ruff-0.7.0-py3-none-win_amd64.whl", hash = "sha256:ff4aabfbaaba880e85d394603b9e75d32b0693152e16fa659a3064a85df7fce2"}, + {file = "ruff-0.7.0-py3-none-win_arm64.whl", hash = "sha256:10842f69c245e78d6adec7e1db0a7d9ddc2fff0621d730e61657b64fa36f207e"}, + {file = "ruff-0.7.0.tar.gz", hash = "sha256:47a86360cf62d9cd53ebfb0b5eb0e882193fc191c6d717e8bef4462bc3b9ea2b"}, +] + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[[package]] +name = "typing-extensions" +version = "4.8.0" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.8.0-py3-none-any.whl", hash = "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0"}, + {file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"}, +] + +[metadata] +lock-version = "2.0" +python-versions = ">=3.10,<4.0" +content-hash = "bdaf4644890fc6f13e55d3d0842e9de9187aa6bfa1965cd2f26e7782aae5c9b3" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..a38488b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,75 @@ +[tool.poetry] +name = "xdg-base-dirs" +version = "6.0.2" +description = "Variables defined by the XDG Base Directory Specification" +authors = ["Scott Stevenson "] +license = "ISC" +readme = "README.md" +homepage = "https://github.com/srstevenson/xdg-base-dirs" +repository = "https://github.com/srstevenson/xdg-base-dirs" +keywords = ["xdg", "base", "directory", "specification"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Natural Language :: English", + "Operating System :: Unix", + "Operating System :: Microsoft :: Windows", +] + +[tool.poetry.dependencies] +python = ">=3.10,<4.0" + +[tool.poetry.group.dev.dependencies] +mypy = ">=1.0.1" +poethepoet = ">=0.22.0" +pytest = ">=7.2.1" +pytest-cov = ">=4.0.0" +ruff = ">=0.1.6" + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" + +[tool.mypy] +check_untyped_defs = true +disallow_any_unimported = true +disallow_untyped_defs = true +enable_error_code = ["ignore-without-code"] +no_implicit_optional = true +show_error_codes = true +strict = true +warn_no_return = true +warn_redundant_casts = true +warn_return_any = true +warn_unreachable = true +warn_unused_configs = true +warn_unused_ignores = true + +[tool.poe.tasks] +_ruff_check_fix = "ruff check --fix ." +_ruff_fmt = "ruff format ." +fmt = ["_ruff_check_fix", "_ruff_fmt"] + +_ruff_fmt_check = "ruff format --check ." +_ruff_check = "ruff check ." +_mypy = "mypy ." +lint = ["_ruff_fmt_check", "_ruff_check", "_mypy"] + +test = "pytest tests" +check = ["lint", "test"] + +[tool.pytest.ini_options] +addopts = "--cov=xdg_base_dirs --cov-report=term-missing" + +[tool.ruff] +target-version = "py310" + +[tool.ruff.format] +skip-magic-trailing-comma = true + +[tool.ruff.lint] +select = ["ALL"] +ignore = ["COM812", "D203", "D213", "INP001", "ISC001", "S101"] + +[tool.ruff.lint.isort] +split-on-trailing-comma = false diff --git a/src/xdg_base_dirs/__init__.py b/src/xdg_base_dirs/__init__.py new file mode 100644 index 0000000..3caa51b --- /dev/null +++ b/src/xdg_base_dirs/__init__.py @@ -0,0 +1,149 @@ +# Copyright © Scott Stevenson +# +# Permission to use, copy, modify, and/or distribute this software for +# any purpose with or without fee is hereby granted, provided that the +# above copyright notice and this permission notice appear in all +# copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL +# WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE +# AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL +# DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR +# PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +# TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +# PERFORMANCE OF THIS SOFTWARE. + +"""XDG Base Directory Specification variables. + +xdg_cache_home(), xdg_config_home(), xdg_data_home(), and xdg_state_home() +return pathlib.Path objects containing the value of the environment variable +named XDG_CACHE_HOME, XDG_CONFIG_HOME, XDG_DATA_HOME, and XDG_STATE_HOME +respectively, or the default defined in the specification if the environment +variable is unset, empty, or contains a relative path rather than absolute +path. + +xdg_config_dirs() and xdg_data_dirs() return a list of pathlib.Path +objects containing the value, split on colons, of the environment +variable named XDG_CONFIG_DIRS and XDG_DATA_DIRS respectively, or the +default defined in the specification if the environment variable is +unset or empty. Relative paths are ignored, as per the specification. + +xdg_runtime_dir() returns a pathlib.Path object containing the value of +the XDG_RUNTIME_DIR environment variable, or None if the environment +variable is not set, or contains a relative path rather than absolute path. + +""" + +import os +from pathlib import Path + +__all__ = [ + "xdg_cache_home", + "xdg_config_dirs", + "xdg_config_home", + "xdg_data_dirs", + "xdg_data_home", + "xdg_runtime_dir", + "xdg_state_home", +] + + +def _path_from_env(variable: str, default: Path) -> Path: + """Read an environment variable as a path. + + The environment variable with the specified name is read, and its + value returned as a path. If the environment variable is not set, is + set to the empty string, or is set to a relative rather than + absolute path, the default value is returned. + + Parameters + ---------- + variable : str + Name of the environment variable. + default : Path + Default value. + + Returns + ------- + Path + Value from environment or default. + + """ + if (value := os.environ.get(variable)) and (path := Path(value)).is_absolute(): + return path + return default + + +def _paths_from_env(variable: str, default: list[Path]) -> list[Path]: + """Read an environment variable as a list of paths. + + The environment variable with the specified name is read, and its + value split on colons and returned as a list of paths. If the + environment variable is not set, or set to the empty string, the + default value is returned. Relative paths are ignored, as per the + specification. + + Parameters + ---------- + variable : str + Name of the environment variable. + default : list[Path] + Default value. + + Returns + ------- + list[Path] + Value from environment or default. + + """ + if value := os.environ.get(variable): + paths = [Path(path) for path in value.split(":") if Path(path).is_absolute()] + if paths: + return paths + return default + + +def xdg_cache_home() -> Path: + """Return a Path corresponding to XDG_CACHE_HOME.""" + return _path_from_env("XDG_CACHE_HOME", Path.home() / ".cache") + + +def xdg_config_dirs() -> list[Path]: + """Return a list of Paths corresponding to XDG_CONFIG_DIRS.""" + return _paths_from_env("XDG_CONFIG_DIRS", [Path("/etc/xdg")]) + + +def xdg_config_home() -> Path: + """Return a Path corresponding to XDG_CONFIG_HOME.""" + return _path_from_env("XDG_CONFIG_HOME", Path.home() / ".config") + + +def xdg_data_dirs() -> list[Path]: + """Return a list of Paths corresponding to XDG_DATA_DIRS.""" + return _paths_from_env( + "XDG_DATA_DIRS", + [Path(path) for path in "/usr/local/share/:/usr/share/".split(":")], + ) + + +def xdg_data_home() -> Path: + """Return a Path corresponding to XDG_DATA_HOME.""" + return _path_from_env("XDG_DATA_HOME", Path.home() / ".local" / "share") + + +def xdg_runtime_dir() -> Path | None: + """Return a Path corresponding to XDG_RUNTIME_DIR. + + If the XDG_RUNTIME_DIR environment variable is not set, None will be + returned as per the specification. + + """ + if (value := os.getenv("XDG_RUNTIME_DIR")) and (path := Path(value)).is_absolute(): + return path + return None + + +def xdg_state_home() -> Path: + """Return a Path corresponding to XDG_STATE_HOME.""" + return _path_from_env("XDG_STATE_HOME", Path.home() / ".local" / "state") diff --git a/src/xdg_base_dirs/py.typed b/src/xdg_base_dirs/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_xdg_base_dirs.py b/tests/test_xdg_base_dirs.py new file mode 100644 index 0000000..045cc38 --- /dev/null +++ b/tests/test_xdg_base_dirs.py @@ -0,0 +1,200 @@ +"""Test suite for xdg-base-dirs.""" + +import os +from pathlib import Path +from typing import Final + +from _pytest.monkeypatch import MonkeyPatch + +import xdg_base_dirs + +HOME_DIR: Final = Path("/homedir") + + +def test_xdg_cache_home_unset(monkeypatch: MonkeyPatch) -> None: + """Test xdg_cache_home when XDG_CACHE_HOME is unset.""" + monkeypatch.delenv("XDG_CACHE_HOME", raising=False) + monkeypatch.setenv("HOME", os.fspath(HOME_DIR)) + assert xdg_base_dirs.xdg_cache_home() == HOME_DIR / ".cache" + + +def test_xdg_cache_home_empty(monkeypatch: MonkeyPatch) -> None: + """Test xdg_cache_home when XDG_CACHE_HOME is empty.""" + monkeypatch.setenv("HOME", os.fspath(HOME_DIR)) + monkeypatch.setenv("XDG_CACHE_HOME", "") + assert xdg_base_dirs.xdg_cache_home() == HOME_DIR / ".cache" + + +def test_xdg_cache_home_relative(monkeypatch: MonkeyPatch) -> None: + """Test xdg_cache_home when XDG_CACHE_HOME is relative path.""" + monkeypatch.setenv("HOME", os.fspath(HOME_DIR)) + monkeypatch.setenv("XDG_CACHE_HOME", "rela/tive") + assert xdg_base_dirs.xdg_cache_home() == HOME_DIR / ".cache" + + +def test_xdg_cache_home_absolute(monkeypatch: MonkeyPatch) -> None: + """Test xdg_cache_home when XDG_CACHE_HOME is absolute path.""" + monkeypatch.setenv("XDG_CACHE_HOME", "/xdg_cache_home") + assert xdg_base_dirs.xdg_cache_home() == Path("/xdg_cache_home") + + +def test_xdg_config_dirs_unset(monkeypatch: MonkeyPatch) -> None: + """Test xdg_config_dirs when XDG_CONFIG_DIRS is unset.""" + monkeypatch.delenv("XDG_CONFIG_DIRS", raising=False) + assert xdg_base_dirs.xdg_config_dirs() == [Path("/etc/xdg")] + + +def test_xdg_config_dirs_empty(monkeypatch: MonkeyPatch) -> None: + """Test xdg_config_dirs when XDG_CONFIG_DIRS is empty.""" + monkeypatch.setenv("XDG_CONFIG_DIRS", "") + assert xdg_base_dirs.xdg_config_dirs() == [Path("/etc/xdg")] + + +def test_xdg_config_dirs_relative(monkeypatch: MonkeyPatch) -> None: + """Test xdg_config_dirs when XDG_CONFIG_DIRS is relative paths.""" + monkeypatch.setenv("XDG_CONFIG_DIRS", "rela/tive:ano/ther") + assert xdg_base_dirs.xdg_config_dirs() == [Path("/etc/xdg")] + + +def test_xdg_config_dirs_set(monkeypatch: MonkeyPatch) -> None: + """Test xdg_config_dirs when XDG_CONFIG_DIRS is set.""" + monkeypatch.setenv("XDG_CONFIG_DIRS", "/first:rela/tive:/sec/ond") + assert xdg_base_dirs.xdg_config_dirs() == [Path("/first"), Path("/sec/ond")] + + +def test_xdg_config_home_unset(monkeypatch: MonkeyPatch) -> None: + """Test xdg_config_home when XDG_CONFIG_HOME is unset.""" + monkeypatch.delenv("XDG_CONFIG_HOME", raising=False) + monkeypatch.setenv("HOME", os.fspath(HOME_DIR)) + assert xdg_base_dirs.xdg_config_home() == HOME_DIR / ".config" + + +def test_xdg_config_home_empty(monkeypatch: MonkeyPatch) -> None: + """Test xdg_config_home when XDG_CONFIG_HOME is empty.""" + monkeypatch.setenv("HOME", os.fspath(HOME_DIR)) + monkeypatch.setenv("XDG_CONFIG_HOME", "") + assert xdg_base_dirs.xdg_config_home() == HOME_DIR / ".config" + + +def test_xdg_config_home_relative(monkeypatch: MonkeyPatch) -> None: + """Test xdg_config_home when XDG_CONFIG_HOME is relative path.""" + monkeypatch.setenv("HOME", os.fspath(HOME_DIR)) + monkeypatch.setenv("XDG_CONFIG_HOME", "rela/tive") + assert xdg_base_dirs.xdg_config_home() == HOME_DIR / ".config" + + +def test_xdg_config_home_absolute(monkeypatch: MonkeyPatch) -> None: + """Test xdg_config_home when XDG_CONFIG_HOME is absolute path.""" + monkeypatch.setenv("XDG_CONFIG_HOME", "/xdg_config_home") + assert xdg_base_dirs.xdg_config_home() == Path("/xdg_config_home") + + +def test_xdg_data_dirs_unset(monkeypatch: MonkeyPatch) -> None: + """Test xdg_data_dirs when XDG_DATA_DIRS is unset.""" + monkeypatch.delenv("XDG_DATA_DIRS", raising=False) + assert xdg_base_dirs.xdg_data_dirs() == [ + Path("/usr/local/share/"), + Path("/usr/share/"), + ] + + +def test_xdg_data_dirs_empty(monkeypatch: MonkeyPatch) -> None: + """Test xdg_data_dirs when XDG_DATA_DIRS is empty.""" + monkeypatch.setenv("XDG_DATA_DIRS", "") + assert xdg_base_dirs.xdg_data_dirs() == [ + Path("/usr/local/share/"), + Path("/usr/share/"), + ] + + +def test_xdg_data_dirs_relative(monkeypatch: MonkeyPatch) -> None: + """Test xdg_data_dirs when XDG_DATA_DIRS is relative paths.""" + monkeypatch.setenv("XDG_DATA_DIRS", "rela/tive:ano/ther") + assert xdg_base_dirs.xdg_data_dirs() == [ + Path("/usr/local/share/"), + Path("/usr/share/"), + ] + + +def test_xdg_data_dirs_set(monkeypatch: MonkeyPatch) -> None: + """Test xdg_data_dirs when XDG_DATA_DIRS is set.""" + monkeypatch.setenv("XDG_DATA_DIRS", "/first/:rela/tive:/sec/ond/") + assert xdg_base_dirs.xdg_data_dirs() == [Path("/first/"), Path("/sec/ond/")] + + +def test_xdg_data_home_unset(monkeypatch: MonkeyPatch) -> None: + """Test xdg_data_home when XDG_DATA_HOME is unset.""" + monkeypatch.delenv("XDG_DATA_HOME", raising=False) + monkeypatch.setenv("HOME", os.fspath(HOME_DIR)) + assert xdg_base_dirs.xdg_data_home() == HOME_DIR / ".local" / "share" + + +def test_xdg_data_home_empty(monkeypatch: MonkeyPatch) -> None: + """Test xdg_data_home when XDG_DATA_HOME is empty.""" + monkeypatch.setenv("HOME", os.fspath(HOME_DIR)) + monkeypatch.setenv("XDG_DATA_HOME", "") + assert xdg_base_dirs.xdg_data_home() == HOME_DIR / ".local" / "share" + + +def test_xdg_data_home_relative(monkeypatch: MonkeyPatch) -> None: + """Test xdg_data_home when XDG_DATA_HOME is relative path.""" + monkeypatch.setenv("HOME", os.fspath(HOME_DIR)) + monkeypatch.setenv("XDG_DATA_HOME", "rela/tive") + assert xdg_base_dirs.xdg_data_home() == HOME_DIR / ".local" / "share" + + +def test_xdg_data_home_absolute(monkeypatch: MonkeyPatch) -> None: + """Test xdg_data_home when XDG_DATA_HOME is absolute path.""" + monkeypatch.setenv("XDG_DATA_HOME", "/xdg_data_home") + assert xdg_base_dirs.xdg_data_home() == Path("/xdg_data_home") + + +def test_xdg_runtime_dir_unset(monkeypatch: MonkeyPatch) -> None: + """Test xdg_runtime_dir when XDG_RUNTIME_DIR is unset.""" + monkeypatch.delenv("XDG_RUNTIME_DIR", raising=False) + assert xdg_base_dirs.xdg_runtime_dir() is None + + +def test_xdg_runtime_dir_empty(monkeypatch: MonkeyPatch) -> None: + """Test xdg_runtime_dir when XDG_RUNTIME_DIR is empty.""" + monkeypatch.setenv("XDG_RUNTIME_DIR", "") + assert xdg_base_dirs.xdg_runtime_dir() is None + + +def test_xdg_runtime_dir_relative(monkeypatch: MonkeyPatch) -> None: + """Test xdg_runtime_dir when XDG_RUNTIME_DIR is relative path.""" + monkeypatch.setenv("XDG_RUNTIME_DIR", "rela/tive") + assert xdg_base_dirs.xdg_runtime_dir() is None + + +def test_xdg_runtime_dir_absolute(monkeypatch: MonkeyPatch) -> None: + """Test xdg_runtime_dir when XDG_RUNTIME_DIR is absolute path.""" + monkeypatch.setenv("XDG_RUNTIME_DIR", "/xdg_runtime_dir") + assert xdg_base_dirs.xdg_runtime_dir() == Path("/xdg_runtime_dir") + + +def test_xdg_state_home_unset(monkeypatch: MonkeyPatch) -> None: + """Test xdg_state_home when XDG_STATE_HOME is unset.""" + monkeypatch.delenv("XDG_STATE_HOME", raising=False) + monkeypatch.setenv("HOME", os.fspath(HOME_DIR)) + assert xdg_base_dirs.xdg_state_home() == HOME_DIR / ".local" / "state" + + +def test_xdg_state_home_empty(monkeypatch: MonkeyPatch) -> None: + """Test xdg_state_home when XDG_STATE_HOME is empty.""" + monkeypatch.setenv("HOME", os.fspath(HOME_DIR)) + monkeypatch.setenv("XDG_STATE_HOME", "") + assert xdg_base_dirs.xdg_state_home() == HOME_DIR / ".local" / "state" + + +def test_xdg_state_home_relative(monkeypatch: MonkeyPatch) -> None: + """Test xdg_state_home when XDG_STATE_HOME is relative path.""" + monkeypatch.setenv("HOME", os.fspath(HOME_DIR)) + monkeypatch.setenv("XDG_STATE_HOME", "rela/tive") + assert xdg_base_dirs.xdg_state_home() == HOME_DIR / ".local" / "state" + + +def test_xdg_state_home_absolute(monkeypatch: MonkeyPatch) -> None: + """Test xdg_state_home when XDG_STATE_HOME is absolute path.""" + monkeypatch.setenv("XDG_STATE_HOME", "/xdg_state_home") + assert xdg_base_dirs.xdg_state_home() == Path("/xdg_state_home")