From 3d4fcbaeb85584dc2f8c3ec7042e1a9d6d215e6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= Date: Sat, 27 Dec 2025 14:46:49 +0000 Subject: [PATCH 01/15] Configure linting --- .github/problem-matchers/sphinx-build.json | 16 + .github/problem-matchers/sphinx-lint.json | 17 + .github/workflows/lint.yaml | 146 ++++ poetry.lock | 914 +++++++++++++++++++-- pyproject.toml | 63 +- 5 files changed, 1082 insertions(+), 74 deletions(-) create mode 100644 .github/problem-matchers/sphinx-build.json create mode 100644 .github/problem-matchers/sphinx-lint.json create mode 100644 .github/workflows/lint.yaml diff --git a/.github/problem-matchers/sphinx-build.json b/.github/problem-matchers/sphinx-build.json new file mode 100644 index 0000000..aff752a --- /dev/null +++ b/.github/problem-matchers/sphinx-build.json @@ -0,0 +1,16 @@ +{ + "problemMatcher": [ + { + "owner": "sphinx-build", + "severity": "error", + "pattern": [ + { + "regexp": "^(/[^:]+):((\\d+):)?(\\sWARNING:)?\\s*(.+)$", + "file": 1, + "line": 3, + "message": 5 + } + ] + } + ] +} diff --git a/.github/problem-matchers/sphinx-lint.json b/.github/problem-matchers/sphinx-lint.json new file mode 100644 index 0000000..44e93e8 --- /dev/null +++ b/.github/problem-matchers/sphinx-lint.json @@ -0,0 +1,17 @@ +{ + "problemMatcher": [ + { + "owner": "sphinx-lint", + "severity": "error", + "pattern": [ + { + "regexp": "^([^:]+):(\\d+):\\s+(.*)\\s\\(([a-z-]+)\\)$", + "file": 1, + "line": 2, + "message": 3, + "code": 4 + } + ] + } + ] +} diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml new file mode 100644 index 0000000..a4169b4 --- /dev/null +++ b/.github/workflows/lint.yaml @@ -0,0 +1,146 @@ +name: Lint check +run-name: Lint code +on: + pull_request: + push: + branches: + - master + +concurrency: + # Cancel previous workflow run when a new commit is pushed to a feature branch + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/master' }} + +env: + PYTHON_VERSION: "3.10" + +jobs: + changed-files: + runs-on: ubuntu-latest + name: Get changed files + outputs: + any_docs_changed: ${{ steps.changed-doc-files.outputs.any_changed }} + any_python_changed: ${{ steps.raw-changed-python-files.outputs.any_changed }} + changed_doc_files: ${{ steps.changed-doc-files.outputs.all_changed_files }} + changed_python_files: ${{ steps.changed-python-files.outputs.all_changed_files }} + steps: + - uses: actions/checkout@v5 + - name: Get changed docs files + id: changed-doc-files + uses: tj-actions/changed-files@v46 + with: + files: | + docs/** + - name: Get changed python files + id: raw-changed-python-files + uses: tj-actions/changed-files@v46 + with: + files: | + **.py + poetry.lock + + - name: Check changed python files + id: changed-python-files + env: + CHANGED_PYTHON_FILES: ${{ steps.raw-changed-python-files.outputs.all_changed_files }} + run: | + if [[ " $CHANGED_PYTHON_FILES " == *" poetry.lock "* ]]; then + # if poetry.lock is changed, we need to check everything + CHANGED_PYTHON_FILES="." + fi + echo "all_changed_files=$CHANGED_PYTHON_FILES" >> "$GITHUB_OUTPUT" + + format: + if: needs.changed-files.outputs.any_python_changed == 'true' + runs-on: ubuntu-latest + name: Check formatting + needs: changed-files + steps: + - uses: actions/checkout@v5 + - name: Install Python tools + uses: BrandonLWhite/pipx-install-action@v1.0.3 + - uses: actions/setup-python@v6 + with: + python-version: ${{ env.PYTHON_VERSION }} + cache: poetry + + - name: Install dependencies + run: poetry install --only=lint + + - name: Check code formatting + # the job output will contain colored diffs with what needs adjusting + run: poe check-format --output-format=github ${{ needs.changed-files.outputs.changed_python_files }} + + lint: + if: needs.changed-files.outputs.any_python_changed == 'true' + runs-on: ubuntu-latest + name: Check linting + needs: changed-files + steps: + - uses: actions/checkout@v5 + - name: Install Python tools + uses: BrandonLWhite/pipx-install-action@v1.0.3 + - uses: actions/setup-python@v6 + with: + python-version: ${{ env.PYTHON_VERSION }} + cache: poetry + + - name: Install dependencies + run: poetry install --only=lint + + - name: Lint code + run: poe lint --output-format=github ${{ needs.changed-files.outputs.changed_python_files }} + + mypy: + if: needs.changed-files.outputs.any_python_changed == 'true' + runs-on: ubuntu-latest + name: Check types with mypy + needs: changed-files + steps: + - uses: actions/checkout@v5 + - name: Install Python tools + uses: BrandonLWhite/pipx-install-action@v1.0.3 + - uses: actions/setup-python@v6 + with: + python-version: ${{ env.PYTHON_VERSION }} + cache: poetry + + - name: Install dependencies + run: poetry install --only=lint + + - name: Type check code + uses: liskin/gh-problem-matcher-wrap@v3 + with: + linters: mypy + run: poe check-types --show-column-numbers --no-error-summary . + + docs: + if: needs.changed-files.outputs.any_docs_changed == 'true' + runs-on: ubuntu-latest + name: Check docs + needs: changed-files + steps: + - uses: actions/checkout@v5 + - name: Install Python tools + uses: BrandonLWhite/pipx-install-action@v1.0.3 + - uses: actions/setup-python@v6 + with: + python-version: ${{ env.PYTHON_VERSION }} + cache: poetry + + - name: Install dependencies + run: poetry install --extras=docs + + - name: Add Sphinx problem matchers + run: | + echo "::add-matcher::.github/problem-matchers/sphinx-build.json" + echo "::add-matcher::.github/problem-matchers/sphinx-lint.json" + + - name: Check docs formatting + run: poe format-docs --check + + - name: Lint docs + run: poe lint-docs + + - name: Build docs + run: poe docs -- -e 'SPHINXOPTS=--fail-on-warning --keep-going' diff --git a/poetry.lock b/poetry.lock index 7565b95..8d8c44e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -4,52 +4,156 @@ name = "alabaster" version = "0.7.16" description = "A light, configurable Sphinx theme" -optional = true +optional = false python-versions = ">=3.9" -groups = ["main"] -markers = "extra == \"docs\"" +groups = ["main", "lint"] files = [ {file = "alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92"}, {file = "alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65"}, ] +markers = {main = "extra == \"docs\""} [[package]] name = "babel" version = "2.17.0" description = "Internationalization utilities" -optional = true +optional = false python-versions = ">=3.8" -groups = ["main"] -markers = "extra == \"docs\"" +groups = ["main", "lint"] files = [ {file = "babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2"}, {file = "babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d"}, ] +markers = {main = "extra == \"docs\""} [package.extras] dev = ["backports.zoneinfo ; python_version < \"3.9\"", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest (>=6.0)", "pytest-cov", "pytz", "setuptools", "tzdata ; sys_platform == \"win32\""] +[[package]] +name = "black" +version = "25.11.0" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.9" +groups = ["lint"] +markers = "python_version < \"3.13\"" +files = [ + {file = "black-25.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ec311e22458eec32a807f029b2646f661e6859c3f61bc6d9ffb67958779f392e"}, + {file = "black-25.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1032639c90208c15711334d681de2e24821af0575573db2810b0763bcd62e0f0"}, + {file = "black-25.11.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0c0f7c461df55cf32929b002335883946a4893d759f2df343389c4396f3b6b37"}, + {file = "black-25.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:f9786c24d8e9bd5f20dc7a7f0cdd742644656987f6ea6947629306f937726c03"}, + {file = "black-25.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:895571922a35434a9d8ca67ef926da6bc9ad464522a5fe0db99b394ef1c0675a"}, + {file = "black-25.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cb4f4b65d717062191bdec8e4a442539a8ea065e6af1c4f4d36f0cdb5f71e170"}, + {file = "black-25.11.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d81a44cbc7e4f73a9d6ae449ec2317ad81512d1e7dce7d57f6333fd6259737bc"}, + {file = "black-25.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:7eebd4744dfe92ef1ee349dc532defbf012a88b087bb7ddd688ff59a447b080e"}, + {file = "black-25.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:80e7486ad3535636657aa180ad32a7d67d7c273a80e12f1b4bfa0823d54e8fac"}, + {file = "black-25.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6cced12b747c4c76bc09b4db057c319d8545307266f41aaee665540bc0e04e96"}, + {file = "black-25.11.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cb2d54a39e0ef021d6c5eef442e10fd71fcb491be6413d083a320ee768329dd"}, + {file = "black-25.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:ae263af2f496940438e5be1a0c1020e13b09154f3af4df0835ea7f9fe7bfa409"}, + {file = "black-25.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0a1d40348b6621cc20d3d7530a5b8d67e9714906dfd7346338249ad9c6cedf2b"}, + {file = "black-25.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:51c65d7d60bb25429ea2bf0731c32b2a2442eb4bd3b2afcb47830f0b13e58bfd"}, + {file = "black-25.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:936c4dd07669269f40b497440159a221ee435e3fddcf668e0c05244a9be71993"}, + {file = "black-25.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:f42c0ea7f59994490f4dccd64e6b2dd49ac57c7c84f38b8faab50f8759db245c"}, + {file = "black-25.11.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:35690a383f22dd3e468c85dc4b915217f87667ad9cce781d7b42678ce63c4170"}, + {file = "black-25.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:dae49ef7369c6caa1a1833fd5efb7c3024bb7e4499bf64833f65ad27791b1545"}, + {file = "black-25.11.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bd4a22a0b37401c8e492e994bce79e614f91b14d9ea911f44f36e262195fdda"}, + {file = "black-25.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:aa211411e94fdf86519996b7f5f05e71ba34835d8f0c0f03c00a26271da02664"}, + {file = "black-25.11.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a3bb5ce32daa9ff0605d73b6f19da0b0e6c1f8f2d75594db539fdfed722f2b06"}, + {file = "black-25.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9815ccee1e55717fe9a4b924cae1646ef7f54e0f990da39a34fc7b264fcf80a2"}, + {file = "black-25.11.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:92285c37b93a1698dcbc34581867b480f1ba3a7b92acf1fe0467b04d7a4da0dc"}, + {file = "black-25.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:43945853a31099c7c0ff8dface53b4de56c41294fa6783c0441a8b1d9bf668bc"}, + {file = "black-25.11.0-py3-none-any.whl", hash = "sha256:e3f562da087791e96cefcd9dda058380a442ab322a02e222add53736451f604b"}, + {file = "black-25.11.0.tar.gz", hash = "sha256:9a323ac32f5dc75ce7470501b887250be5005a01602e931a15e45593f70f6e08"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" +pytokens = ">=0.3.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.10)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "black" +version = "25.12.0" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.10" +groups = ["lint"] +markers = "python_version >= \"3.13\"" +files = [ + {file = "black-25.12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f85ba1ad15d446756b4ab5f3044731bf68b777f8f9ac9cdabd2425b97cd9c4e8"}, + {file = "black-25.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:546eecfe9a3a6b46f9d69d8a642585a6eaf348bcbbc4d87a19635570e02d9f4a"}, + {file = "black-25.12.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:17dcc893da8d73d8f74a596f64b7c98ef5239c2cd2b053c0f25912c4494bf9ea"}, + {file = "black-25.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:09524b0e6af8ba7a3ffabdfc7a9922fb9adef60fed008c7cd2fc01f3048e6e6f"}, + {file = "black-25.12.0-cp310-cp310-win_arm64.whl", hash = "sha256:b162653ed89eb942758efeb29d5e333ca5bb90e5130216f8369857db5955a7da"}, + {file = "black-25.12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d0cfa263e85caea2cff57d8f917f9f51adae8e20b610e2b23de35b5b11ce691a"}, + {file = "black-25.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1a2f578ae20c19c50a382286ba78bfbeafdf788579b053d8e4980afb079ab9be"}, + {file = "black-25.12.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e1b65634b0e471d07ff86ec338819e2ef860689859ef4501ab7ac290431f9b"}, + {file = "black-25.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:a3fa71e3b8dd9f7c6ac4d818345237dfb4175ed3bf37cd5a581dbc4c034f1ec5"}, + {file = "black-25.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:51e267458f7e650afed8445dc7edb3187143003d52a1b710c7321aef22aa9655"}, + {file = "black-25.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:31f96b7c98c1ddaeb07dc0f56c652e25bdedaac76d5b68a059d998b57c55594a"}, + {file = "black-25.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:05dd459a19e218078a1f98178c13f861fe6a9a5f88fc969ca4d9b49eb1809783"}, + {file = "black-25.12.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1f68c5eff61f226934be6b5b80296cf6939e5d2f0c2f7d543ea08b204bfaf59"}, + {file = "black-25.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:274f940c147ddab4442d316b27f9e332ca586d39c85ecf59ebdea82cc9ee8892"}, + {file = "black-25.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:169506ba91ef21e2e0591563deda7f00030cb466e747c4b09cb0a9dae5db2f43"}, + {file = "black-25.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a05ddeb656534c3e27a05a29196c962877c83fa5503db89e68857d1161ad08a5"}, + {file = "black-25.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9ec77439ef3e34896995503865a85732c94396edcc739f302c5673a2315e1e7f"}, + {file = "black-25.12.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e509c858adf63aa61d908061b52e580c40eae0dfa72415fa47ac01b12e29baf"}, + {file = "black-25.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:252678f07f5bac4ff0d0e9b261fbb029fa530cfa206d0a636a34ab445ef8ca9d"}, + {file = "black-25.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:bc5b1c09fe3c931ddd20ee548511c64ebf964ada7e6f0763d443947fd1c603ce"}, + {file = "black-25.12.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:0a0953b134f9335c2434864a643c842c44fba562155c738a2a37a4d61f00cad5"}, + {file = "black-25.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2355bbb6c3b76062870942d8cc450d4f8ac71f9c93c40122762c8784df49543f"}, + {file = "black-25.12.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9678bd991cc793e81d19aeeae57966ee02909877cb65838ccffef24c3ebac08f"}, + {file = "black-25.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:97596189949a8aad13ad12fcbb4ae89330039b96ad6742e6f6b45e75ad5cfd83"}, + {file = "black-25.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:778285d9ea197f34704e3791ea9404cd6d07595745907dd2ce3da7a13627b29b"}, + {file = "black-25.12.0-py3-none-any.whl", hash = "sha256:48ceb36c16dbc84062740049eef990bb2ce07598272e673c17d1a7720c71c828"}, + {file = "black-25.12.0.tar.gz", hash = "sha256:8d3dd9cea14bff7ddc0eb243c811cdb1a011ebb4800a5f0335a01a68654796a7"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" +pytokens = ">=0.3.0" + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.10)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + [[package]] name = "certifi" version = "2025.10.5" description = "Python package for providing Mozilla's CA Bundle." -optional = true +optional = false python-versions = ">=3.7" -groups = ["main"] -markers = "extra == \"docs\"" +groups = ["main", "lint"] files = [ {file = "certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de"}, {file = "certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43"}, ] +markers = {main = "extra == \"docs\""} [[package]] name = "charset-normalizer" version = "3.4.4" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -optional = true +optional = false python-versions = ">=3.7" -groups = ["main"] -markers = "extra == \"docs\"" +groups = ["main", "lint"] files = [ {file = "charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d"}, {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8"}, @@ -165,6 +269,39 @@ files = [ {file = "charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f"}, {file = "charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a"}, ] +markers = {main = "extra == \"docs\""} + +[[package]] +name = "click" +version = "8.1.8" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +groups = ["lint"] +markers = "python_version < \"3.13\"" +files = [ + {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, + {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "click" +version = "8.3.1" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.10" +groups = ["lint"] +markers = "python_version >= \"3.13\"" +files = [ + {file = "click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6"}, + {file = "click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} [[package]] name = "colorama" @@ -172,12 +309,12 @@ 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" -groups = ["main", "dev"] +groups = ["main", "dev", "lint"] files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] -markers = {main = "extra == \"docs\" and sys_platform == \"win32\"", dev = "sys_platform == \"win32\""} +markers = {main = "extra == \"docs\" and sys_platform == \"win32\"", dev = "sys_platform == \"win32\"", lint = "sys_platform == \"win32\" or platform_system == \"Windows\""} [[package]] name = "coverage" @@ -299,18 +436,48 @@ tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.1 [package.extras] toml = ["tomli ; python_full_version <= \"3.11.0a6\""] +[[package]] +name = "docstrfmt" +version = "1.11.1" +description = "docstrfmt: A formatter for Sphinx flavored reStructuredText." +optional = false +python-versions = ">=3.9" +groups = ["lint"] +files = [ + {file = "docstrfmt-1.11.1-py3-none-any.whl", hash = "sha256:6782d8663321c3a7c40be08a36fbcb1ea9e46d1efba85411ba807d97f384871a"}, + {file = "docstrfmt-1.11.1.tar.gz", hash = "sha256:d41e19d6c5d524cc7f8ff6cbfecb8762d77e696b9fe4f5057269051fb966fc80"}, +] + +[package.dependencies] +black = ">=24" +click = ">=8" +docutils = ">=0.20" +libcst = ">=1" +platformdirs = ">=4" +roman = "*" +sphinx = ">=7" +tabulate = ">=0.9" +toml = {version = ">=0.10", markers = "python_version < \"3.11\""} + +[package.extras] +ci = ["coveralls"] +d = ["aiohttp (>=3)"] +dev = ["docstrfmt[lint]", "docstrfmt[test]", "packaging"] +lint = ["pre-commit", "ruff (>=0.0.292)"] +test = ["pytest", "pytest-aiohttp"] + [[package]] name = "docutils" version = "0.21.2" description = "Docutils -- Python Documentation Utilities" -optional = true +optional = false python-versions = ">=3.9" -groups = ["main"] -markers = "extra == \"docs\"" +groups = ["main", "lint"] files = [ {file = "docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2"}, {file = "docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f"}, ] +markers = {main = "extra == \"docs\""} [[package]] name = "exceptiongroup" @@ -335,14 +502,14 @@ test = ["pytest (>=6)"] name = "idna" version = "3.11" description = "Internationalized Domain Names in Applications (IDNA)" -optional = true +optional = false python-versions = ">=3.8" -groups = ["main"] -markers = "extra == \"docs\"" +groups = ["main", "lint"] files = [ {file = "idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea"}, {file = "idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"}, ] +markers = {main = "extra == \"docs\""} [package.extras] all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] @@ -351,27 +518,27 @@ all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2 name = "imagesize" version = "1.4.1" description = "Getting image size from png/jpeg/jpeg2000/gif file" -optional = true +optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -groups = ["main"] -markers = "extra == \"docs\"" +groups = ["main", "lint"] files = [ {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, ] +markers = {main = "extra == \"docs\""} [[package]] name = "importlib-metadata" version = "8.7.0" description = "Read metadata from Python packages" -optional = true +optional = false python-versions = ">=3.9" -groups = ["main"] -markers = "extra == \"docs\" and python_version == \"3.9\"" +groups = ["main", "lint"] files = [ {file = "importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd"}, {file = "importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000"}, ] +markers = {main = "extra == \"docs\" and python_version == \"3.9\"", lint = "python_version == \"3.9\""} [package.dependencies] zipp = ">=3.20" @@ -401,14 +568,14 @@ files = [ name = "jinja2" version = "3.1.6" description = "A very fast and expressive template engine." -optional = true +optional = false python-versions = ">=3.7" -groups = ["main"] -markers = "extra == \"docs\"" +groups = ["main", "lint"] files = [ {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, ] +markers = {main = "extra == \"docs\""} [package.dependencies] MarkupSafe = ">=2.0" @@ -416,14 +583,183 @@ MarkupSafe = ">=2.0" [package.extras] i18n = ["Babel (>=2.7)"] +[[package]] +name = "libcst" +version = "1.8.6" +description = "A concrete syntax tree with AST-like properties for Python 3.0 through 3.14 programs." +optional = false +python-versions = ">=3.9" +groups = ["lint"] +files = [ + {file = "libcst-1.8.6-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a20c5182af04332cc94d8520792befda06d73daf2865e6dddc5161c72ea92cb9"}, + {file = "libcst-1.8.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:36473e47cb199b7e6531d653ee6ffed057de1d179301e6c67f651f3af0b499d6"}, + {file = "libcst-1.8.6-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:06fc56335a45d61b7c1b856bfab4587b84cfe31e9d6368f60bb3c9129d900f58"}, + {file = "libcst-1.8.6-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:6b23d14a7fc0addd9795795763af26b185deb7c456b1e7cc4d5228e69dab5ce8"}, + {file = "libcst-1.8.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:16cfe0cfca5fd840e1fb2c30afb628b023d3085b30c3484a79b61eae9d6fe7ba"}, + {file = "libcst-1.8.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:455f49a93aea4070132c30ebb6c07c2dea0ba6c1fde5ffde59fc45dbb9cfbe4b"}, + {file = "libcst-1.8.6-cp310-cp310-win_amd64.whl", hash = "sha256:72cca15800ffc00ba25788e4626189fe0bc5fe2a0c1cb4294bce2e4df21cc073"}, + {file = "libcst-1.8.6-cp310-cp310-win_arm64.whl", hash = "sha256:6cad63e3a26556b020b634d25a8703b605c0e0b491426b3e6b9e12ed20f09100"}, + {file = "libcst-1.8.6-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:3649a813660fbffd7bc24d3f810b1f75ac98bd40d9d6f56d1f0ee38579021073"}, + {file = "libcst-1.8.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0cbe17067055829607c5ba4afa46bfa4d0dd554c0b5a583546e690b7367a29b6"}, + {file = "libcst-1.8.6-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:59a7e388c57d21d63722018978a8ddba7b176e3a99bd34b9b84a576ed53f2978"}, + {file = "libcst-1.8.6-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:b6c1248cc62952a3a005792b10cdef2a4e130847be9c74f33a7d617486f7e532"}, + {file = "libcst-1.8.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6421a930b028c5ef4a943b32a5a78b7f1bf15138214525a2088f11acbb7d3d64"}, + {file = "libcst-1.8.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6d8b67874f2188399a71a71731e1ba2d1a2c3173b7565d1cc7ffb32e8fbaba5b"}, + {file = "libcst-1.8.6-cp311-cp311-win_amd64.whl", hash = "sha256:b0d8c364c44ae343937f474b2e492c1040df96d94530377c2f9263fb77096e4f"}, + {file = "libcst-1.8.6-cp311-cp311-win_arm64.whl", hash = "sha256:5dcaaebc835dfe5755bc85f9b186fb7e2895dda78e805e577fef1011d51d5a5c"}, + {file = "libcst-1.8.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0c13d5bd3d8414a129e9dccaf0e5785108a4441e9b266e1e5e9d1f82d1b943c9"}, + {file = "libcst-1.8.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f1472eeafd67cdb22544e59cf3bfc25d23dc94058a68cf41f6654ff4fcb92e09"}, + {file = "libcst-1.8.6-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:089c58e75cb142ec33738a1a4ea7760a28b40c078ab2fd26b270dac7d2633a4d"}, + {file = "libcst-1.8.6-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:c9d7aeafb1b07d25a964b148c0dda9451efb47bbbf67756e16eeae65004b0eb5"}, + {file = "libcst-1.8.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:207481197afd328aa91d02670c15b48d0256e676ce1ad4bafb6dc2b593cc58f1"}, + {file = "libcst-1.8.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:375965f34cc6f09f5f809244d3ff9bd4f6cb6699f571121cebce53622e7e0b86"}, + {file = "libcst-1.8.6-cp312-cp312-win_amd64.whl", hash = "sha256:da95b38693b989eaa8d32e452e8261cfa77fe5babfef1d8d2ac25af8c4aa7e6d"}, + {file = "libcst-1.8.6-cp312-cp312-win_arm64.whl", hash = "sha256:bff00e1c766658adbd09a175267f8b2f7616e5ee70ce45db3d7c4ce6d9f6bec7"}, + {file = "libcst-1.8.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7445479ebe7d1aff0ee094ab5a1c7718e1ad78d33e3241e1a1ec65dcdbc22ffb"}, + {file = "libcst-1.8.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4fc3fef8a2c983e7abf5d633e1884c5dd6fa0dcb8f6e32035abd3d3803a3a196"}, + {file = "libcst-1.8.6-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:1a3a5e4ee870907aa85a4076c914ae69066715a2741b821d9bf16f9579de1105"}, + {file = "libcst-1.8.6-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:6609291c41f7ad0bac570bfca5af8fea1f4a27987d30a1fa8b67fe5e67e6c78d"}, + {file = "libcst-1.8.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:25eaeae6567091443b5374b4c7d33a33636a2d58f5eda02135e96fc6c8807786"}, + {file = "libcst-1.8.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04030ea4d39d69a65873b1d4d877def1c3951a7ada1824242539e399b8763d30"}, + {file = "libcst-1.8.6-cp313-cp313-win_amd64.whl", hash = "sha256:8066f1b70f21a2961e96bedf48649f27dfd5ea68be5cd1bed3742b047f14acde"}, + {file = "libcst-1.8.6-cp313-cp313-win_arm64.whl", hash = "sha256:c188d06b583900e662cd791a3f962a8c96d3dfc9b36ea315be39e0a4c4792ebf"}, + {file = "libcst-1.8.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:c41c76e034a1094afed7057023b1d8967f968782433f7299cd170eaa01ec033e"}, + {file = "libcst-1.8.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5432e785322aba3170352f6e72b32bea58d28abd141ac37cc9b0bf6b7c778f58"}, + {file = "libcst-1.8.6-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:85b7025795b796dea5284d290ff69de5089fc8e989b25d6f6f15b6800be7167f"}, + {file = "libcst-1.8.6-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:536567441182a62fb706e7aa954aca034827b19746832205953b2c725d254a93"}, + {file = "libcst-1.8.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2f04d3672bde1704f383a19e8f8331521abdbc1ed13abb349325a02ac56e5012"}, + {file = "libcst-1.8.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f04febcd70e1e67917be7de513c8d4749d2e09206798558d7fe632134426ea4"}, + {file = "libcst-1.8.6-cp313-cp313t-win_amd64.whl", hash = "sha256:1dc3b897c8b0f7323412da3f4ad12b16b909150efc42238e19cbf19b561cc330"}, + {file = "libcst-1.8.6-cp313-cp313t-win_arm64.whl", hash = "sha256:44f38139fa95e488db0f8976f9c7ca39a64d6bc09f2eceef260aa1f6da6a2e42"}, + {file = "libcst-1.8.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:b188e626ce61de5ad1f95161b8557beb39253de4ec74fc9b1f25593324a0279c"}, + {file = "libcst-1.8.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:87e74f7d7dfcba9efa91127081e22331d7c42515f0a0ac6e81d4cf2c3ed14661"}, + {file = "libcst-1.8.6-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:3a926a4b42015ee24ddfc8ae940c97bd99483d286b315b3ce82f3bafd9f53474"}, + {file = "libcst-1.8.6-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:3f4fbb7f569e69fd9e89d9d9caa57ca42c577c28ed05062f96a8c207594e75b8"}, + {file = "libcst-1.8.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:08bd63a8ce674be431260649e70fca1d43f1554f1591eac657f403ff8ef82c7a"}, + {file = "libcst-1.8.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e00e275d4ba95d4963431ea3e409aa407566a74ee2bf309a402f84fc744abe47"}, + {file = "libcst-1.8.6-cp314-cp314-win_amd64.whl", hash = "sha256:fea5c7fa26556eedf277d4f72779c5ede45ac3018650721edd77fd37ccd4a2d4"}, + {file = "libcst-1.8.6-cp314-cp314-win_arm64.whl", hash = "sha256:bb9b4077bdf8857b2483879cbbf70f1073bc255b057ec5aac8a70d901bb838e9"}, + {file = "libcst-1.8.6-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:55ec021a296960c92e5a33b8d93e8ad4182b0eab657021f45262510a58223de1"}, + {file = "libcst-1.8.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ba9ab2b012fbd53b36cafd8f4440a6b60e7e487cd8b87428e57336b7f38409a4"}, + {file = "libcst-1.8.6-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c0a0cc80aebd8aa15609dd4d330611cbc05e9b4216bcaeabba7189f99ef07c28"}, + {file = "libcst-1.8.6-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:42a4f68121e2e9c29f49c97f6154e8527cd31021809cc4a941c7270aa64f41aa"}, + {file = "libcst-1.8.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a434c521fadaf9680788b50d5c21f4048fa85ed19d7d70bd40549fbaeeecab1"}, + {file = "libcst-1.8.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6a65f844d813ab4ef351443badffa0ae358f98821561d19e18b3190f59e71996"}, + {file = "libcst-1.8.6-cp314-cp314t-win_amd64.whl", hash = "sha256:bdb14bc4d4d83a57062fed2c5da93ecb426ff65b0dc02ddf3481040f5f074a82"}, + {file = "libcst-1.8.6-cp314-cp314t-win_arm64.whl", hash = "sha256:819c8081e2948635cab60c603e1bbdceccdfe19104a242530ad38a36222cb88f"}, + {file = "libcst-1.8.6-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:cb2679ef532f9fa5be5c5a283b6357cb6e9888a8dd889c4bb2b01845a29d8c0b"}, + {file = "libcst-1.8.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:203ec2a83f259baf686b9526268cd23d048d38be5589594ef143aee50a4faf7e"}, + {file = "libcst-1.8.6-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:6366ab2107425bf934b0c83311177f2a371bfc757ee8c6ad4a602d7cbcc2f363"}, + {file = "libcst-1.8.6-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:6aa11df6c58812f731172b593fcb485d7ba09ccc3b52fea6c7f26a43377dc748"}, + {file = "libcst-1.8.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:351ab879c2fd20d9cb2844ed1ea3e617ed72854d3d1e2b0880ede9c3eea43ba8"}, + {file = "libcst-1.8.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:98fa1ca321c81fb1f02e5c43f956ca543968cc1a30b264fd8e0a2e1b0b0bf106"}, + {file = "libcst-1.8.6-cp39-cp39-win_amd64.whl", hash = "sha256:25fc7a1303cad7639ad45ec38c06789b4540b7258e9a108924aaa2c132af4aca"}, + {file = "libcst-1.8.6-cp39-cp39-win_arm64.whl", hash = "sha256:4d7bbdd35f3abdfb5ac5d1a674923572dab892b126a58da81ff2726102d6ec2e"}, + {file = "libcst-1.8.6.tar.gz", hash = "sha256:f729c37c9317126da9475bdd06a7208eb52fcbd180a6341648b45a56b4ba708b"}, +] + +[package.dependencies] +pyyaml = [ + {version = ">=5.2", markers = "python_version < \"3.13\""}, + {version = ">=6.0.3", markers = "python_version >= \"3.14\""}, +] +pyyaml-ft = {version = ">=8.0.0", markers = "python_version == \"3.13\""} +typing-extensions = {version = "*", markers = "python_version < \"3.10\""} + +[[package]] +name = "librt" +version = "0.7.5" +description = "Mypyc runtime library" +optional = false +python-versions = ">=3.9" +groups = ["lint"] +markers = "platform_python_implementation != \"PyPy\"" +files = [ + {file = "librt-0.7.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:81056e01bba1394f1d92904ec61a4078f66df785316275edbaf51d90da8c6e26"}, + {file = "librt-0.7.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d7c72c8756eeb3aefb1b9e3dac7c37a4a25db63640cac0ab6fc18e91a0edf05a"}, + {file = "librt-0.7.5-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ddc4a16207f88f9597b397fc1f60781266d13b13de922ff61c206547a29e4bbd"}, + {file = "librt-0.7.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:63055d3dda433ebb314c9f1819942f16a19203c454508fdb2d167613f7017169"}, + {file = "librt-0.7.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9f85f9b5db87b0f52e53c68ad2a0c5a53e00afa439bd54a1723742a2b1021276"}, + {file = "librt-0.7.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c566a4672564c5d54d8ab65cdaae5a87ee14c1564c1a2ddc7a9f5811c750f023"}, + {file = "librt-0.7.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fee15c2a190ef389f14928135c6fb2d25cd3fdb7887bfd9a7b444bbdc8c06b96"}, + {file = "librt-0.7.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:584cb3e605ec45ba350962cec853e17be0a25a772f21f09f1e422f7044ae2a7d"}, + {file = "librt-0.7.5-cp310-cp310-win32.whl", hash = "sha256:9c08527055fbb03c641c15bbc5b79dd2942fb6a3bd8dabf141dd7e97eeea4904"}, + {file = "librt-0.7.5-cp310-cp310-win_amd64.whl", hash = "sha256:dd810f2d39c526c42ea205e0addad5dc08ef853c625387806a29d07f9d150d9b"}, + {file = "librt-0.7.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f952e1a78c480edee8fb43aa2bf2e84dcd46c917d44f8065b883079d3893e8fc"}, + {file = "librt-0.7.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75965c1f4efb7234ff52a58b729d245a21e87e4b6a26a0ec08052f02b16274e4"}, + {file = "librt-0.7.5-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:732e0aa0385b59a1b2545159e781c792cc58ce9c134249233a7c7250a44684c4"}, + {file = "librt-0.7.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cdde31759bd8888f3ef0eebda80394a48961328a17c264dce8cc35f4b9cde35d"}, + {file = "librt-0.7.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df3146d52465b3b6397d25d513f428cb421c18df65b7378667bb5f1e3cc45805"}, + {file = "librt-0.7.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:29c8d2fae11d4379ea207ba7fc69d43237e42cf8a9f90ec6e05993687e6d648b"}, + {file = "librt-0.7.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bb41f04046b4f22b1e7ba5ef513402cd2e3477ec610e5f92d38fe2bba383d419"}, + {file = "librt-0.7.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8bb7883c1e94ceb87c2bf81385266f032da09cd040e804cc002f2c9d6b842e2f"}, + {file = "librt-0.7.5-cp311-cp311-win32.whl", hash = "sha256:84d4a6b9efd6124f728558a18e79e7cc5c5d4efc09b2b846c910de7e564f5bad"}, + {file = "librt-0.7.5-cp311-cp311-win_amd64.whl", hash = "sha256:ab4b0d3bee6f6ff7017e18e576ac7e41a06697d8dea4b8f3ab9e0c8e1300c409"}, + {file = "librt-0.7.5-cp311-cp311-win_arm64.whl", hash = "sha256:730be847daad773a3c898943cf67fb9845a3961d06fb79672ceb0a8cd8624cfa"}, + {file = "librt-0.7.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ba1077c562a046208a2dc6366227b3eeae8f2c2ab4b41eaf4fd2fa28cece4203"}, + {file = "librt-0.7.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:654fdc971c76348a73af5240d8e2529265b9a7ba6321e38dd5bae7b0d4ab3abe"}, + {file = "librt-0.7.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6b7b58913d475911f6f33e8082f19dd9b120c4f4a5c911d07e395d67b81c6982"}, + {file = "librt-0.7.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8e0fd344bad57026a8f4ccfaf406486c2fc991838050c2fef156170edc3b775"}, + {file = "librt-0.7.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:46aa91813c267c3f60db75d56419b42c0c0b9748ec2c568a0e3588e543fb4233"}, + {file = "librt-0.7.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ddc0ab9dbc5f9ceaf2bf7a367bf01f2697660e908f6534800e88f43590b271db"}, + {file = "librt-0.7.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7a488908a470451338607650f1c064175094aedebf4a4fa37890682e30ce0b57"}, + {file = "librt-0.7.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e47fc52602ffc374e69bf1b76536dc99f7f6dd876bd786c8213eaa3598be030a"}, + {file = "librt-0.7.5-cp312-cp312-win32.whl", hash = "sha256:cda8b025875946ffff5a9a7590bf9acde3eb02cb6200f06a2d3e691ef3d9955b"}, + {file = "librt-0.7.5-cp312-cp312-win_amd64.whl", hash = "sha256:b591c094afd0ffda820e931148c9e48dc31a556dc5b2b9b3cc552fa710d858e4"}, + {file = "librt-0.7.5-cp312-cp312-win_arm64.whl", hash = "sha256:532ddc6a8a6ca341b1cd7f4d999043e4c71a212b26fe9fd2e7f1e8bb4e873544"}, + {file = "librt-0.7.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b1795c4b2789b458fa290059062c2f5a297ddb28c31e704d27e161386469691a"}, + {file = "librt-0.7.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2fcbf2e135c11f721193aa5f42ba112bb1046afafbffd407cbc81d8d735c74d0"}, + {file = "librt-0.7.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c039bbf79a9a2498404d1ae7e29a6c175e63678d7a54013a97397c40aee026c5"}, + {file = "librt-0.7.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3919c9407faeeee35430ae135e3a78acd4ecaaaa73767529e2c15ca1d73ba325"}, + {file = "librt-0.7.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:26b46620e1e0e45af510d9848ea0915e7040605dd2ae94ebefb6c962cbb6f7ec"}, + {file = "librt-0.7.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9bbb8facc5375476d392990dd6a71f97e4cb42e2ac66f32e860f6e47299d5e89"}, + {file = "librt-0.7.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e9e9c988b5ffde7be02180f864cbd17c0b0c1231c235748912ab2afa05789c25"}, + {file = "librt-0.7.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:edf6b465306215b19dbe6c3fb63cf374a8f3e1ad77f3b4c16544b83033bbb67b"}, + {file = "librt-0.7.5-cp313-cp313-win32.whl", hash = "sha256:060bde69c3604f694bd8ae21a780fe8be46bb3dbb863642e8dfc75c931ca8eee"}, + {file = "librt-0.7.5-cp313-cp313-win_amd64.whl", hash = "sha256:a82d5a0ee43aeae2116d7292c77cc8038f4841830ade8aa922e098933b468b9e"}, + {file = "librt-0.7.5-cp313-cp313-win_arm64.whl", hash = "sha256:3c98a8d0ac9e2a7cb8ff8c53e5d6e8d82bfb2839abf144fdeaaa832f2a12aa45"}, + {file = "librt-0.7.5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:9937574e6d842f359b8585903d04f5b4ab62277a091a93e02058158074dc52f2"}, + {file = "librt-0.7.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5cd3afd71e9bc146203b6c8141921e738364158d4aa7cdb9a874e2505163770f"}, + {file = "librt-0.7.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9cffa3ef0af29687455161cb446eff059bf27607f95163d6a37e27bcb37180f6"}, + {file = "librt-0.7.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:82f3f088482e2229387eadf8215c03f7726d56f69cce8c0c40f0795aebc9b361"}, + {file = "librt-0.7.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7aa33153a5bb0bac783d2c57885889b1162823384e8313d47800a0e10d0070e"}, + {file = "librt-0.7.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:265729b551a2dd329cc47b323a182fb7961af42abf21e913c9dd7d3331b2f3c2"}, + {file = "librt-0.7.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:168e04663e126416ba712114050f413ac306759a1791d87b7c11d4428ba75760"}, + {file = "librt-0.7.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:553dc58987d1d853adda8aeadf4db8e29749f0b11877afcc429a9ad892818ae2"}, + {file = "librt-0.7.5-cp314-cp314-win32.whl", hash = "sha256:263f4fae9eba277513357c871275b18d14de93fd49bf5e43dc60a97b81ad5eb8"}, + {file = "librt-0.7.5-cp314-cp314-win_amd64.whl", hash = "sha256:85f485b7471571e99fab4f44eeb327dc0e1f814ada575f3fa85e698417d8a54e"}, + {file = "librt-0.7.5-cp314-cp314-win_arm64.whl", hash = "sha256:49c596cd18e90e58b7caa4d7ca7606049c1802125fcff96b8af73fa5c3870e4d"}, + {file = "librt-0.7.5-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:54d2aef0b0f5056f130981ad45081b278602ff3657fe16c88529f5058038e802"}, + {file = "librt-0.7.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0b4791202296ad51ac09a3ff58eb49d9da8e3a4009167a6d76ac418a974e5fd4"}, + {file = "librt-0.7.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e860909fea75baef941ee6436e0453612505883b9d0d87924d4fda27865b9a2"}, + {file = "librt-0.7.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f02c4337bf271c4f06637f5ff254fad2238c0b8e32a3a480ebb2fc5e26f754a5"}, + {file = "librt-0.7.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7f51ffe59f4556243d3cc82d827bde74765f594fa3ceb80ec4de0c13ccd3416"}, + {file = "librt-0.7.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0b7f080ba30601dfa3e3deed3160352273e1b9bc92e652f51103c3e9298f7899"}, + {file = "librt-0.7.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:fb565b4219abc8ea2402e61c7ba648a62903831059ed3564fa1245cc245d58d7"}, + {file = "librt-0.7.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a3cfb15961e7333ea6ef033dc574af75153b5c230d5ad25fbcd55198f21e0cf"}, + {file = "librt-0.7.5-cp314-cp314t-win32.whl", hash = "sha256:118716de5ad6726332db1801bc90fa6d94194cd2e07c1a7822cebf12c496714d"}, + {file = "librt-0.7.5-cp314-cp314t-win_amd64.whl", hash = "sha256:3dd58f7ce20360c6ce0c04f7bd9081c7f9c19fc6129a3c705d0c5a35439f201d"}, + {file = "librt-0.7.5-cp314-cp314t-win_arm64.whl", hash = "sha256:08153ea537609d11f774d2bfe84af39d50d5c9ca3a4d061d946e0c9d8bce04a1"}, + {file = "librt-0.7.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:df2e210400b28e50994477ebf82f055698c79797b6ee47a1669d383ca33263e1"}, + {file = "librt-0.7.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d2cc7d187e8c6e9b7bdbefa9697ce897a704ea7a7ce844f2b4e0e2aa07ae51d3"}, + {file = "librt-0.7.5-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:39183abee670bc37b85f11e86c44a9cad1ed6efa48b580083e89ecee13dd9717"}, + {file = "librt-0.7.5-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:191cbd42660446d67cf7a95ac7bfa60f49b8b3b0417c64f216284a1d86fc9335"}, + {file = "librt-0.7.5-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ea1b60b86595a5dc1f57b44a801a1c4d8209c0a69518391d349973a4491408e6"}, + {file = "librt-0.7.5-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:af69d9e159575e877c7546d1ee817b4ae089aa221dd1117e20c24ad8dc8659c7"}, + {file = "librt-0.7.5-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:0e2bf8f91093fac43e3eaebacf777f12fd539dce9ec5af3efc6d8424e96ccd49"}, + {file = "librt-0.7.5-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8dcae24de1bc9da93aa689cb6313c70e776d7cea2fcf26b9b6160fedfe6bd9af"}, + {file = "librt-0.7.5-cp39-cp39-win32.whl", hash = "sha256:cdb001a1a0e4f41e613bca2c0fc147fc8a7396f53fc94201cbfd8ec7cd69ca4b"}, + {file = "librt-0.7.5-cp39-cp39-win_amd64.whl", hash = "sha256:a9eacbf983319b26b5f340a2e0cd47ac1ee4725a7f3a72fd0f15063c934b69d6"}, + {file = "librt-0.7.5.tar.gz", hash = "sha256:de4221a1181fa9c8c4b5f35506ed6f298948f44003d84d2a8b9885d7e01e6cfa"}, +] + [[package]] name = "markupsafe" version = "3.0.3" description = "Safely add untrusted strings to HTML/XML markup." -optional = true +optional = false python-versions = ">=3.9" -groups = ["main"] -markers = "extra == \"docs\"" +groups = ["main", "lint"] files = [ {file = "markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559"}, {file = "markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419"}, @@ -515,6 +851,81 @@ files = [ {file = "markupsafe-3.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8"}, {file = "markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698"}, ] +markers = {main = "extra == \"docs\""} + +[[package]] +name = "mypy" +version = "1.19.1" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.9" +groups = ["lint"] +files = [ + {file = "mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec"}, + {file = "mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b"}, + {file = "mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6"}, + {file = "mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74"}, + {file = "mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1"}, + {file = "mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac"}, + {file = "mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288"}, + {file = "mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab"}, + {file = "mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6"}, + {file = "mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331"}, + {file = "mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925"}, + {file = "mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042"}, + {file = "mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1"}, + {file = "mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e"}, + {file = "mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2"}, + {file = "mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8"}, + {file = "mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a"}, + {file = "mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13"}, + {file = "mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250"}, + {file = "mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b"}, + {file = "mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e"}, + {file = "mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef"}, + {file = "mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75"}, + {file = "mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd"}, + {file = "mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1"}, + {file = "mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718"}, + {file = "mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b"}, + {file = "mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045"}, + {file = "mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957"}, + {file = "mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f"}, + {file = "mypy-1.19.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7bcfc336a03a1aaa26dfce9fff3e287a3ba99872a157561cbfcebe67c13308e3"}, + {file = "mypy-1.19.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b7951a701c07ea584c4fe327834b92a30825514c868b1f69c30445093fdd9d5a"}, + {file = "mypy-1.19.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b13cfdd6c87fc3efb69ea4ec18ef79c74c3f98b4e5498ca9b85ab3b2c2329a67"}, + {file = "mypy-1.19.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f28f99c824ecebcdaa2e55d82953e38ff60ee5ec938476796636b86afa3956e"}, + {file = "mypy-1.19.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c608937067d2fc5a4dd1a5ce92fd9e1398691b8c5d012d66e1ddd430e9244376"}, + {file = "mypy-1.19.1-cp39-cp39-win_amd64.whl", hash = "sha256:409088884802d511ee52ca067707b90c883426bd95514e8cfda8281dc2effe24"}, + {file = "mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247"}, + {file = "mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba"}, +] + +[package.dependencies] +librt = {version = ">=0.6.2", markers = "platform_python_implementation != \"PyPy\""} +mypy_extensions = ">=1.0.0" +pathspec = ">=0.9.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing_extensions = ">=4.6.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +faster-cache = ["orjson"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.8" +groups = ["lint"] +files = [ + {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, + {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, +] [[package]] name = "packaging" @@ -522,13 +933,61 @@ version = "25.0" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" -groups = ["main", "dev"] +groups = ["main", "dev", "lint"] files = [ {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, ] markers = {main = "extra == \"docs\""} +[[package]] +name = "pathspec" +version = "0.12.1" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.8" +groups = ["lint"] +files = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] + +[[package]] +name = "platformdirs" +version = "4.4.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.9" +groups = ["lint"] +markers = "python_version < \"3.13\"" +files = [ + {file = "platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85"}, + {file = "platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.14.1)"] + +[[package]] +name = "platformdirs" +version = "4.5.1" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.10" +groups = ["lint"] +markers = "python_version >= \"3.13\"" +files = [ + {file = "platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31"}, + {file = "platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda"}, +] + +[package.extras] +docs = ["furo (>=2025.9.25)", "proselint (>=0.14)", "sphinx (>=8.2.3)", "sphinx-autodoc-typehints (>=3.2)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.4.2)", "pytest-cov (>=7)", "pytest-mock (>=3.15.1)"] +type = ["mypy (>=1.18.2)"] + [[package]] name = "pluggy" version = "1.6.0" @@ -545,13 +1004,25 @@ files = [ dev = ["pre-commit", "tox"] testing = ["coverage", "pytest", "pytest-benchmark"] +[[package]] +name = "polib" +version = "1.2.0" +description = "A library to manipulate gettext files (po and mo files)." +optional = false +python-versions = "*" +groups = ["lint"] +files = [ + {file = "polib-1.2.0-py2.py3-none-any.whl", hash = "sha256:1c77ee1b81feb31df9bca258cbc58db1bbb32d10214b173882452c73af06d62d"}, + {file = "polib-1.2.0.tar.gz", hash = "sha256:f3ef94aefed6e183e342a8a269ae1fc4742ba193186ad76f175938621dbfc26b"}, +] + [[package]] name = "pygments" version = "2.19.2" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.8" -groups = ["main", "dev"] +groups = ["main", "dev", "lint"] files = [ {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, @@ -605,14 +1076,36 @@ pytest = ">=7" [package.extras] testing = ["process-tests", "pytest-xdist", "virtualenv"] +[[package]] +name = "pytokens" +version = "0.3.0" +description = "A Fast, spec compliant Python 3.14+ tokenizer that runs on older Pythons." +optional = false +python-versions = ">=3.8" +groups = ["lint"] +files = [ + {file = "pytokens-0.3.0-py3-none-any.whl", hash = "sha256:95b2b5eaf832e469d141a378872480ede3f251a5a5041b8ec6e581d3ac71bbf3"}, + {file = "pytokens-0.3.0.tar.gz", hash = "sha256:2f932b14ed08de5fcf0b391ace2642f858f1394c0857202959000b68ed7a458a"}, +] + +[package.extras] +dev = ["black", "build", "mypy", "pytest", "pytest-cov", "setuptools", "tox", "twine", "wheel"] + [[package]] name = "pyyaml" version = "6.0.3" description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.8" -groups = ["main"] +groups = ["main", "lint"] files = [ + {file = "PyYAML-6.0.3-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f"}, + {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4"}, + {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3"}, + {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6"}, + {file = "PyYAML-6.0.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369"}, + {file = "PyYAML-6.0.3-cp38-cp38-win32.whl", hash = "sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295"}, + {file = "PyYAML-6.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b"}, {file = "pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b"}, {file = "pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956"}, {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8"}, @@ -680,19 +1173,173 @@ files = [ {file = "pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007"}, {file = "pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f"}, ] +markers = {lint = "python_version < \"3.13\" or python_version >= \"3.14\""} + +[[package]] +name = "pyyaml-ft" +version = "8.0.0" +description = "YAML parser and emitter for Python with support for free-threading" +optional = false +python-versions = ">=3.13" +groups = ["lint"] +markers = "python_version == \"3.13\"" +files = [ + {file = "pyyaml_ft-8.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8c1306282bc958bfda31237f900eb52c9bedf9b93a11f82e1aab004c9a5657a6"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:30c5f1751625786c19de751e3130fc345ebcba6a86f6bddd6e1285342f4bbb69"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3fa992481155ddda2e303fcc74c79c05eddcdbc907b888d3d9ce3ff3e2adcfb0"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cec6c92b4207004b62dfad1f0be321c9f04725e0f271c16247d8b39c3bf3ea42"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06237267dbcab70d4c0e9436d8f719f04a51123f0ca2694c00dd4b68c338e40b"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8a7f332bc565817644cdb38ffe4739e44c3e18c55793f75dddb87630f03fc254"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7d10175a746be65f6feb86224df5d6bc5c049ebf52b89a88cf1cd78af5a367a8"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:58e1015098cf8d8aec82f360789c16283b88ca670fe4275ef6c48c5e30b22a96"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:e64fa5f3e2ceb790d50602b2fd4ec37abbd760a8c778e46354df647e7c5a4ebb"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8d445bf6ea16bb93c37b42fdacfb2f94c8e92a79ba9e12768c96ecde867046d1"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c56bb46b4fda34cbb92a9446a841da3982cdde6ea13de3fbd80db7eeeab8b49"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dab0abb46eb1780da486f022dce034b952c8ae40753627b27a626d803926483b"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd48d639cab5ca50ad957b6dd632c7dd3ac02a1abe0e8196a3c24a52f5db3f7a"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:052561b89d5b2a8e1289f326d060e794c21fa068aa11255fe71d65baf18a632e"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3bb4b927929b0cb162fb1605392a321e3333e48ce616cdcfa04a839271373255"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:de04cfe9439565e32f178106c51dd6ca61afaa2907d143835d501d84703d3793"}, + {file = "pyyaml_ft-8.0.0.tar.gz", hash = "sha256:0c947dce03954c7b5d38869ed4878b2e6ff1d44b08a0d84dc83fdad205ae39ab"}, +] + +[[package]] +name = "regex" +version = "2025.11.3" +description = "Alternative regular expression module, to replace re." +optional = false +python-versions = ">=3.9" +groups = ["lint"] +files = [ + {file = "regex-2025.11.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2b441a4ae2c8049106e8b39973bfbddfb25a179dda2bdb99b0eeb60c40a6a3af"}, + {file = "regex-2025.11.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2fa2eed3f76677777345d2f81ee89f5de2f5745910e805f7af7386a920fa7313"}, + {file = "regex-2025.11.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d8b4a27eebd684319bdf473d39f1d79eed36bf2cd34bd4465cdb4618d82b3d56"}, + {file = "regex-2025.11.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cf77eac15bd264986c4a2c63353212c095b40f3affb2bc6b4ef80c4776c1a28"}, + {file = "regex-2025.11.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b7f9ee819f94c6abfa56ec7b1dbab586f41ebbdc0a57e6524bd5e7f487a878c7"}, + {file = "regex-2025.11.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:838441333bc90b829406d4a03cb4b8bf7656231b84358628b0406d803931ef32"}, + {file = "regex-2025.11.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cfe6d3f0c9e3b7e8c0c694b24d25e677776f5ca26dce46fd6b0489f9c8339391"}, + {file = "regex-2025.11.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2ab815eb8a96379a27c3b6157fcb127c8f59c36f043c1678110cea492868f1d5"}, + {file = "regex-2025.11.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:728a9d2d173a65b62bdc380b7932dd8e74ed4295279a8fe1021204ce210803e7"}, + {file = "regex-2025.11.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:509dc827f89c15c66a0c216331260d777dd6c81e9a4e4f830e662b0bb296c313"}, + {file = "regex-2025.11.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:849202cd789e5f3cf5dcc7822c34b502181b4824a65ff20ce82da5524e45e8e9"}, + {file = "regex-2025.11.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b6f78f98741dcc89607c16b1e9426ee46ce4bf31ac5e6b0d40e81c89f3481ea5"}, + {file = "regex-2025.11.3-cp310-cp310-win32.whl", hash = "sha256:149eb0bba95231fb4f6d37c8f760ec9fa6fabf65bab555e128dde5f2475193ec"}, + {file = "regex-2025.11.3-cp310-cp310-win_amd64.whl", hash = "sha256:ee3a83ce492074c35a74cc76cf8235d49e77b757193a5365ff86e3f2f93db9fd"}, + {file = "regex-2025.11.3-cp310-cp310-win_arm64.whl", hash = "sha256:38af559ad934a7b35147716655d4a2f79fcef2d695ddfe06a06ba40ae631fa7e"}, + {file = "regex-2025.11.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eadade04221641516fa25139273505a1c19f9bf97589a05bc4cfcd8b4a618031"}, + {file = "regex-2025.11.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:feff9e54ec0dd3833d659257f5c3f5322a12eee58ffa360984b716f8b92983f4"}, + {file = "regex-2025.11.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3b30bc921d50365775c09a7ed446359e5c0179e9e2512beec4a60cbcef6ddd50"}, + {file = "regex-2025.11.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f99be08cfead2020c7ca6e396c13543baea32343b7a9a5780c462e323bd8872f"}, + {file = "regex-2025.11.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6dd329a1b61c0ee95ba95385fb0c07ea0d3fe1a21e1349fa2bec272636217118"}, + {file = "regex-2025.11.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4c5238d32f3c5269d9e87be0cf096437b7622b6920f5eac4fd202468aaeb34d2"}, + {file = "regex-2025.11.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10483eefbfb0adb18ee9474498c9a32fcf4e594fbca0543bb94c48bac6183e2e"}, + {file = "regex-2025.11.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:78c2d02bb6e1da0720eedc0bad578049cad3f71050ef8cd065ecc87691bed2b0"}, + {file = "regex-2025.11.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e6b49cd2aad93a1790ce9cffb18964f6d3a4b0b3dbdbd5de094b65296fce6e58"}, + {file = "regex-2025.11.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:885b26aa3ee56433b630502dc3d36ba78d186a00cc535d3806e6bfd9ed3c70ab"}, + {file = "regex-2025.11.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ddd76a9f58e6a00f8772e72cff8ebcff78e022be95edf018766707c730593e1e"}, + {file = "regex-2025.11.3-cp311-cp311-win32.whl", hash = "sha256:3e816cc9aac1cd3cc9a4ec4d860f06d40f994b5c7b4d03b93345f44e08cc68bf"}, + {file = "regex-2025.11.3-cp311-cp311-win_amd64.whl", hash = "sha256:087511f5c8b7dfbe3a03f5d5ad0c2a33861b1fc387f21f6f60825a44865a385a"}, + {file = "regex-2025.11.3-cp311-cp311-win_arm64.whl", hash = "sha256:1ff0d190c7f68ae7769cd0313fe45820ba07ffebfddfaa89cc1eb70827ba0ddc"}, + {file = "regex-2025.11.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bc8ab71e2e31b16e40868a40a69007bc305e1109bd4658eb6cad007e0bf67c41"}, + {file = "regex-2025.11.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:22b29dda7e1f7062a52359fca6e58e548e28c6686f205e780b02ad8ef710de36"}, + {file = "regex-2025.11.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3a91e4a29938bc1a082cc28fdea44be420bf2bebe2665343029723892eb073e1"}, + {file = "regex-2025.11.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08b884f4226602ad40c5d55f52bf91a9df30f513864e0054bad40c0e9cf1afb7"}, + {file = "regex-2025.11.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3e0b11b2b2433d1c39c7c7a30e3f3d0aeeea44c2a8d0bae28f6b95f639927a69"}, + {file = "regex-2025.11.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:87eb52a81ef58c7ba4d45c3ca74e12aa4b4e77816f72ca25258a85b3ea96cb48"}, + {file = "regex-2025.11.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a12ab1f5c29b4e93db518f5e3872116b7e9b1646c9f9f426f777b50d44a09e8c"}, + {file = "regex-2025.11.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7521684c8c7c4f6e88e35ec89680ee1aa8358d3f09d27dfbdf62c446f5d4c695"}, + {file = "regex-2025.11.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7fe6e5440584e94cc4b3f5f4d98a25e29ca12dccf8873679a635638349831b98"}, + {file = "regex-2025.11.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:8e026094aa12b43f4fd74576714e987803a315c76edb6b098b9809db5de58f74"}, + {file = "regex-2025.11.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:435bbad13e57eb5606a68443af62bed3556de2f46deb9f7d4237bc2f1c9fb3a0"}, + {file = "regex-2025.11.3-cp312-cp312-win32.whl", hash = "sha256:3839967cf4dc4b985e1570fd8d91078f0c519f30491c60f9ac42a8db039be204"}, + {file = "regex-2025.11.3-cp312-cp312-win_amd64.whl", hash = "sha256:e721d1b46e25c481dc5ded6f4b3f66c897c58d2e8cfdf77bbced84339108b0b9"}, + {file = "regex-2025.11.3-cp312-cp312-win_arm64.whl", hash = "sha256:64350685ff08b1d3a6fff33f45a9ca183dc1d58bbfe4981604e70ec9801bbc26"}, + {file = "regex-2025.11.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c1e448051717a334891f2b9a620fe36776ebf3dd8ec46a0b877c8ae69575feb4"}, + {file = "regex-2025.11.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9b5aca4d5dfd7fbfbfbdaf44850fcc7709a01146a797536a8f84952e940cca76"}, + {file = "regex-2025.11.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:04d2765516395cf7dda331a244a3282c0f5ae96075f728629287dfa6f76ba70a"}, + {file = "regex-2025.11.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d9903ca42bfeec4cebedba8022a7c97ad2aab22e09573ce9976ba01b65e4361"}, + {file = "regex-2025.11.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:639431bdc89d6429f6721625e8129413980ccd62e9d3f496be618a41d205f160"}, + {file = "regex-2025.11.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f117efad42068f9715677c8523ed2be1518116d1c49b1dd17987716695181efe"}, + {file = "regex-2025.11.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4aecb6f461316adf9f1f0f6a4a1a3d79e045f9b71ec76055a791affa3b285850"}, + {file = "regex-2025.11.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3b3a5f320136873cc5561098dfab677eea139521cb9a9e8db98b7e64aef44cbc"}, + {file = "regex-2025.11.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:75fa6f0056e7efb1f42a1c34e58be24072cb9e61a601340cc1196ae92326a4f9"}, + {file = "regex-2025.11.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:dbe6095001465294f13f1adcd3311e50dd84e5a71525f20a10bd16689c61ce0b"}, + {file = "regex-2025.11.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:454d9b4ae7881afbc25015b8627c16d88a597479b9dea82b8c6e7e2e07240dc7"}, + {file = "regex-2025.11.3-cp313-cp313-win32.whl", hash = "sha256:28ba4d69171fc6e9896337d4fc63a43660002b7da53fc15ac992abcf3410917c"}, + {file = "regex-2025.11.3-cp313-cp313-win_amd64.whl", hash = "sha256:bac4200befe50c670c405dc33af26dad5a3b6b255dd6c000d92fe4629f9ed6a5"}, + {file = "regex-2025.11.3-cp313-cp313-win_arm64.whl", hash = "sha256:2292cd5a90dab247f9abe892ac584cb24f0f54680c73fcb4a7493c66c2bf2467"}, + {file = "regex-2025.11.3-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:1eb1ebf6822b756c723e09f5186473d93236c06c579d2cc0671a722d2ab14281"}, + {file = "regex-2025.11.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1e00ec2970aab10dc5db34af535f21fcf32b4a31d99e34963419636e2f85ae39"}, + {file = "regex-2025.11.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a4cb042b615245d5ff9b3794f56be4138b5adc35a4166014d31d1814744148c7"}, + {file = "regex-2025.11.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44f264d4bf02f3176467d90b294d59bf1db9fe53c141ff772f27a8b456b2a9ed"}, + {file = "regex-2025.11.3-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7be0277469bf3bd7a34a9c57c1b6a724532a0d235cd0dc4e7f4316f982c28b19"}, + {file = "regex-2025.11.3-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0d31e08426ff4b5b650f68839f5af51a92a5b51abd8554a60c2fbc7c71f25d0b"}, + {file = "regex-2025.11.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e43586ce5bd28f9f285a6e729466841368c4a0353f6fd08d4ce4630843d3648a"}, + {file = "regex-2025.11.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:0f9397d561a4c16829d4e6ff75202c1c08b68a3bdbfe29dbfcdb31c9830907c6"}, + {file = "regex-2025.11.3-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:dd16e78eb18ffdb25ee33a0682d17912e8cc8a770e885aeee95020046128f1ce"}, + {file = "regex-2025.11.3-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:ffcca5b9efe948ba0661e9df0fa50d2bc4b097c70b9810212d6b62f05d83b2dd"}, + {file = "regex-2025.11.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c56b4d162ca2b43318ac671c65bd4d563e841a694ac70e1a976ac38fcf4ca1d2"}, + {file = "regex-2025.11.3-cp313-cp313t-win32.whl", hash = "sha256:9ddc42e68114e161e51e272f667d640f97e84a2b9ef14b7477c53aac20c2d59a"}, + {file = "regex-2025.11.3-cp313-cp313t-win_amd64.whl", hash = "sha256:7a7c7fdf755032ffdd72c77e3d8096bdcb0eb92e89e17571a196f03d88b11b3c"}, + {file = "regex-2025.11.3-cp313-cp313t-win_arm64.whl", hash = "sha256:df9eb838c44f570283712e7cff14c16329a9f0fb19ca492d21d4b7528ee6821e"}, + {file = "regex-2025.11.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:9697a52e57576c83139d7c6f213d64485d3df5bf84807c35fa409e6c970801c6"}, + {file = "regex-2025.11.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e18bc3f73bd41243c9b38a6d9f2366cd0e0137a9aebe2d8ff76c5b67d4c0a3f4"}, + {file = "regex-2025.11.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:61a08bcb0ec14ff4e0ed2044aad948d0659604f824cbd50b55e30b0ec6f09c73"}, + {file = "regex-2025.11.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9c30003b9347c24bcc210958c5d167b9e4f9be786cb380a7d32f14f9b84674f"}, + {file = "regex-2025.11.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4e1e592789704459900728d88d41a46fe3969b82ab62945560a31732ffc19a6d"}, + {file = "regex-2025.11.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6538241f45eb5a25aa575dbba1069ad786f68a4f2773a29a2bd3dd1f9de787be"}, + {file = "regex-2025.11.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce22519c989bb72a7e6b36a199384c53db7722fe669ba891da75907fe3587db"}, + {file = "regex-2025.11.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:66d559b21d3640203ab9075797a55165d79017520685fb407b9234d72ab63c62"}, + {file = "regex-2025.11.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:669dcfb2e38f9e8c69507bace46f4889e3abbfd9b0c29719202883c0a603598f"}, + {file = "regex-2025.11.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:32f74f35ff0f25a5021373ac61442edcb150731fbaa28286bbc8bb1582c89d02"}, + {file = "regex-2025.11.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e6c7a21dffba883234baefe91bc3388e629779582038f75d2a5be918e250f0ed"}, + {file = "regex-2025.11.3-cp314-cp314-win32.whl", hash = "sha256:795ea137b1d809eb6836b43748b12634291c0ed55ad50a7d72d21edf1cd565c4"}, + {file = "regex-2025.11.3-cp314-cp314-win_amd64.whl", hash = "sha256:9f95fbaa0ee1610ec0fc6b26668e9917a582ba80c52cc6d9ada15e30aa9ab9ad"}, + {file = "regex-2025.11.3-cp314-cp314-win_arm64.whl", hash = "sha256:dfec44d532be4c07088c3de2876130ff0fbeeacaa89a137decbbb5f665855a0f"}, + {file = "regex-2025.11.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ba0d8a5d7f04f73ee7d01d974d47c5834f8a1b0224390e4fe7c12a3a92a78ecc"}, + {file = "regex-2025.11.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:442d86cf1cfe4faabf97db7d901ef58347efd004934da045c745e7b5bd57ac49"}, + {file = "regex-2025.11.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:fd0a5e563c756de210bb964789b5abe4f114dacae9104a47e1a649b910361536"}, + {file = "regex-2025.11.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf3490bcbb985a1ae97b2ce9ad1c0f06a852d5b19dde9b07bdf25bf224248c95"}, + {file = "regex-2025.11.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3809988f0a8b8c9dcc0f92478d6501fac7200b9ec56aecf0ec21f4a2ec4b6009"}, + {file = "regex-2025.11.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f4ff94e58e84aedb9c9fce66d4ef9f27a190285b451420f297c9a09f2b9abee9"}, + {file = "regex-2025.11.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eb542fd347ce61e1321b0a6b945d5701528dca0cd9759c2e3bb8bd57e47964d"}, + {file = "regex-2025.11.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d6c2d5919075a1f2e413c00b056ea0c2f065b3f5fe83c3d07d325ab92dce51d6"}, + {file = "regex-2025.11.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3f8bf11a4827cc7ce5a53d4ef6cddd5ad25595d3c1435ef08f76825851343154"}, + {file = "regex-2025.11.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:22c12d837298651e5550ac1d964e4ff57c3f56965fc1812c90c9fb2028eaf267"}, + {file = "regex-2025.11.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:62ba394a3dda9ad41c7c780f60f6e4a70988741415ae96f6d1bf6c239cf01379"}, + {file = "regex-2025.11.3-cp314-cp314t-win32.whl", hash = "sha256:4bf146dca15cdd53224a1bf46d628bd7590e4a07fbb69e720d561aea43a32b38"}, + {file = "regex-2025.11.3-cp314-cp314t-win_amd64.whl", hash = "sha256:adad1a1bcf1c9e76346e091d22d23ac54ef28e1365117d99521631078dfec9de"}, + {file = "regex-2025.11.3-cp314-cp314t-win_arm64.whl", hash = "sha256:c54f768482cef41e219720013cd05933b6f971d9562544d691c68699bf2b6801"}, + {file = "regex-2025.11.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:81519e25707fc076978c6143b81ea3dc853f176895af05bf7ec51effe818aeec"}, + {file = "regex-2025.11.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3bf28b1873a8af8bbb58c26cc56ea6e534d80053b41fb511a35795b6de507e6a"}, + {file = "regex-2025.11.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:856a25c73b697f2ce2a24e7968285579e62577a048526161a2c0f53090bea9f9"}, + {file = "regex-2025.11.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a3d571bd95fade53c86c0517f859477ff3a93c3fde10c9e669086f038e0f207"}, + {file = "regex-2025.11.3-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:732aea6de26051af97b94bc98ed86448821f839d058e5d259c72bf6d73ad0fc0"}, + {file = "regex-2025.11.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:51c1c1847128238f54930edb8805b660305dca164645a9fd29243f5610beea34"}, + {file = "regex-2025.11.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22dd622a402aad4558277305350699b2be14bc59f64d64ae1d928ce7d072dced"}, + {file = "regex-2025.11.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f3b5a391c7597ffa96b41bd5cbd2ed0305f515fcbb367dfa72735679d5502364"}, + {file = "regex-2025.11.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:cc4076a5b4f36d849fd709284b4a3b112326652f3b0466f04002a6c15a0c96c1"}, + {file = "regex-2025.11.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:a295ca2bba5c1c885826ce3125fa0b9f702a1be547d821c01d65f199e10c01e2"}, + {file = "regex-2025.11.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:b4774ff32f18e0504bfc4e59a3e71e18d83bc1e171a3c8ed75013958a03b2f14"}, + {file = "regex-2025.11.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:22e7d1cdfa88ef33a2ae6aa0d707f9255eb286ffbd90045f1088246833223aee"}, + {file = "regex-2025.11.3-cp39-cp39-win32.whl", hash = "sha256:74d04244852ff73b32eeede4f76f51c5bcf44bc3c207bc3e6cf1c5c45b890708"}, + {file = "regex-2025.11.3-cp39-cp39-win_amd64.whl", hash = "sha256:7a50cd39f73faa34ec18d6720ee25ef10c4c1839514186fcda658a06c06057a2"}, + {file = "regex-2025.11.3-cp39-cp39-win_arm64.whl", hash = "sha256:43b4fb020e779ca81c1b5255015fe2b82816c76ec982354534ad9ec09ad7c9e3"}, + {file = "regex-2025.11.3.tar.gz", hash = "sha256:1fedc720f9bb2494ce31a58a1631f9c82df6a09b49c19517ea5cc280b4541e01"}, +] [[package]] name = "requests" version = "2.32.5" description = "Python HTTP for Humans." -optional = true +optional = false python-versions = ">=3.9" -groups = ["main"] -markers = "extra == \"docs\"" +groups = ["main", "lint"] files = [ {file = "requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"}, {file = "requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"}, ] +markers = {main = "extra == \"docs\""} [package.dependencies] certifi = ">=2017.4.17" @@ -704,31 +1351,86 @@ urllib3 = ">=1.21.1,<3" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +[[package]] +name = "roman" +version = "5.1" +description = "Integer to Roman numerals converter" +optional = false +python-versions = ">=3.9" +groups = ["lint"] +markers = "python_version < \"3.13\"" +files = [ + {file = "roman-5.1-py3-none-any.whl", hash = "sha256:bf595d8a9bc4a8e8b1dfa23e1d4def0251b03b494786df6b8c3d3f1635ce285a"}, + {file = "roman-5.1.tar.gz", hash = "sha256:3a86572e9bc9183e771769601189e5fa32f1620ffeceebb9eca836affb409986"}, +] + +[[package]] +name = "roman" +version = "5.2" +description = "Integer to Roman numerals converter" +optional = false +python-versions = ">=3.10" +groups = ["lint"] +markers = "python_version >= \"3.13\"" +files = [ + {file = "roman-5.2-py3-none-any.whl", hash = "sha256:89d3b47400388806d06ff77ea77c79ab080bc127820dea6bf34e1f1c1b8e676e"}, + {file = "roman-5.2.tar.gz", hash = "sha256:275fe9f46290f7d0ffaea1c33251b92b8e463ace23660508ceef522e7587cb6f"}, +] + +[[package]] +name = "ruff" +version = "0.14.10" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +groups = ["lint"] +files = [ + {file = "ruff-0.14.10-py3-none-linux_armv6l.whl", hash = "sha256:7a3ce585f2ade3e1f29ec1b92df13e3da262178df8c8bdf876f48fa0e8316c49"}, + {file = "ruff-0.14.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:674f9be9372907f7257c51f1d4fc902cb7cf014b9980152b802794317941f08f"}, + {file = "ruff-0.14.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d85713d522348837ef9df8efca33ccb8bd6fcfc86a2cde3ccb4bc9d28a18003d"}, + {file = "ruff-0.14.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6987ebe0501ae4f4308d7d24e2d0fe3d7a98430f5adfd0f1fead050a740a3a77"}, + {file = "ruff-0.14.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:16a01dfb7b9e4eee556fbfd5392806b1b8550c9b4a9f6acd3dbe6812b193c70a"}, + {file = "ruff-0.14.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7165d31a925b7a294465fa81be8c12a0e9b60fb02bf177e79067c867e71f8b1f"}, + {file = "ruff-0.14.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c561695675b972effb0c0a45db233f2c816ff3da8dcfbe7dfc7eed625f218935"}, + {file = "ruff-0.14.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4bb98fcbbc61725968893682fd4df8966a34611239c9fd07a1f6a07e7103d08e"}, + {file = "ruff-0.14.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f24b47993a9d8cb858429e97bdf8544c78029f09b520af615c1d261bf827001d"}, + {file = "ruff-0.14.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59aabd2e2c4fd614d2862e7939c34a532c04f1084476d6833dddef4afab87e9f"}, + {file = "ruff-0.14.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:213db2b2e44be8625002dbea33bb9c60c66ea2c07c084a00d55732689d697a7f"}, + {file = "ruff-0.14.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b914c40ab64865a17a9a5b67911d14df72346a634527240039eb3bd650e5979d"}, + {file = "ruff-0.14.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1484983559f026788e3a5c07c81ef7d1e97c1c78ed03041a18f75df104c45405"}, + {file = "ruff-0.14.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c70427132db492d25f982fffc8d6c7535cc2fd2c83fc8888f05caaa248521e60"}, + {file = "ruff-0.14.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5bcf45b681e9f1ee6445d317ce1fa9d6cba9a6049542d1c3d5b5958986be8830"}, + {file = "ruff-0.14.10-py3-none-win32.whl", hash = "sha256:104c49fc7ab73f3f3a758039adea978869a918f31b73280db175b43a2d9b51d6"}, + {file = "ruff-0.14.10-py3-none-win_amd64.whl", hash = "sha256:466297bd73638c6bdf06485683e812db1c00c7ac96d4ddd0294a338c62fdc154"}, + {file = "ruff-0.14.10-py3-none-win_arm64.whl", hash = "sha256:e51d046cf6dda98a4633b8a8a771451107413b0f07183b2bef03f075599e44e6"}, + {file = "ruff-0.14.10.tar.gz", hash = "sha256:9a2e830f075d1a42cd28420d7809ace390832a490ed0966fe373ba288e77aaf4"}, +] + [[package]] name = "snowballstemmer" version = "3.0.1" description = "This package provides 32 stemmers for 30 languages generated from Snowball algorithms." -optional = true +optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*" -groups = ["main"] -markers = "extra == \"docs\"" +groups = ["main", "lint"] files = [ {file = "snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064"}, {file = "snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895"}, ] +markers = {main = "extra == \"docs\""} [[package]] name = "sphinx" version = "7.4.7" description = "Python documentation generator" -optional = true +optional = false python-versions = ">=3.9" -groups = ["main"] -markers = "extra == \"docs\"" +groups = ["main", "lint"] files = [ {file = "sphinx-7.4.7-py3-none-any.whl", hash = "sha256:c2419e2135d11f1951cd994d6eb18a1835bd8fdd8429f9ca375dc1f3281bd239"}, {file = "sphinx-7.4.7.tar.gz", hash = "sha256:242f92a7ea7e6c5b406fdc2615413890ba9f699114a9c09192d7dfead2ee9cfe"}, ] +markers = {main = "extra == \"docs\""} [package.dependencies] alabaster = ">=0.7.14,<0.8.0" @@ -755,6 +1457,46 @@ docs = ["sphinxcontrib-websupport"] lint = ["flake8 (>=6.0)", "importlib-metadata (>=6.0)", "mypy (==1.10.1)", "pytest (>=6.0)", "ruff (==0.5.2)", "sphinx-lint (>=0.9)", "tomli (>=2)", "types-docutils (==0.21.0.20240711)", "types-requests (>=2.30.0)"] test = ["cython (>=3.0)", "defusedxml (>=0.7.1)", "pytest (>=8.0)", "setuptools (>=70.0)", "typing_extensions (>=4.9)"] +[[package]] +name = "sphinx-lint" +version = "1.0.0" +description = "Check for stylistic and formal issues in .rst and .py files included in the documentation." +optional = false +python-versions = ">=3.8" +groups = ["lint"] +markers = "python_version < \"3.13\"" +files = [ + {file = "sphinx_lint-1.0.0-py3-none-any.whl", hash = "sha256:6117a0f340b2dc73eadfc57db7531d4477e0929f92a0c1a2f61e6edbc272f0bc"}, + {file = "sphinx_lint-1.0.0.tar.gz", hash = "sha256:6eafdb44172ce526f405bf36c713eb246f1340ec2d667e7298e2487ed76decd2"}, +] + +[package.dependencies] +polib = "*" +regex = "*" + +[package.extras] +tests = ["pytest", "pytest-cov"] + +[[package]] +name = "sphinx-lint" +version = "1.0.2" +description = "Check for stylistic and formal issues in .rst and .py files included in the documentation." +optional = false +python-versions = ">=3.10" +groups = ["lint"] +markers = "python_version >= \"3.13\"" +files = [ + {file = "sphinx_lint-1.0.2-py3-none-any.whl", hash = "sha256:edcd0fa4d916386c5a3ef7ef0f5136f0bb4a15feefc83c1068ba15bc16eec652"}, + {file = "sphinx_lint-1.0.2.tar.gz", hash = "sha256:4e7fc12f44f750b0006eaad237d7db9b1d8aba92adda9c838af891654b371d35"}, +] + +[package.dependencies] +polib = "*" +regex = "*" + +[package.extras] +tests = ["pytest", "pytest-cov"] + [[package]] name = "sphinx-rtd-theme" version = "3.0.2" @@ -780,14 +1522,14 @@ dev = ["bump2version", "transifex-client", "twine", "wheel"] name = "sphinxcontrib-applehelp" version = "2.0.0" description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" -optional = true +optional = false python-versions = ">=3.9" -groups = ["main"] -markers = "extra == \"docs\"" +groups = ["main", "lint"] files = [ {file = "sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5"}, {file = "sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1"}, ] +markers = {main = "extra == \"docs\""} [package.extras] lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] @@ -798,14 +1540,14 @@ test = ["pytest"] name = "sphinxcontrib-devhelp" version = "2.0.0" description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp documents" -optional = true +optional = false python-versions = ">=3.9" -groups = ["main"] -markers = "extra == \"docs\"" +groups = ["main", "lint"] files = [ {file = "sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2"}, {file = "sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad"}, ] +markers = {main = "extra == \"docs\""} [package.extras] lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] @@ -816,14 +1558,14 @@ test = ["pytest"] name = "sphinxcontrib-htmlhelp" version = "2.1.0" description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" -optional = true +optional = false python-versions = ">=3.9" -groups = ["main"] -markers = "extra == \"docs\"" +groups = ["main", "lint"] files = [ {file = "sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8"}, {file = "sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9"}, ] +markers = {main = "extra == \"docs\""} [package.extras] lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] @@ -850,14 +1592,14 @@ Sphinx = ">=1.8" name = "sphinxcontrib-jsmath" version = "1.0.1" description = "A sphinx extension which renders display math in HTML via JavaScript" -optional = true +optional = false python-versions = ">=3.5" -groups = ["main"] -markers = "extra == \"docs\"" +groups = ["main", "lint"] files = [ {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, ] +markers = {main = "extra == \"docs\""} [package.extras] test = ["flake8", "mypy", "pytest"] @@ -866,14 +1608,14 @@ test = ["flake8", "mypy", "pytest"] name = "sphinxcontrib-qthelp" version = "2.0.0" description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp documents" -optional = true +optional = false python-versions = ">=3.9" -groups = ["main"] -markers = "extra == \"docs\"" +groups = ["main", "lint"] files = [ {file = "sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb"}, {file = "sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab"}, ] +markers = {main = "extra == \"docs\""} [package.extras] lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] @@ -884,27 +1626,55 @@ test = ["defusedxml (>=0.7.1)", "pytest"] name = "sphinxcontrib-serializinghtml" version = "2.0.0" description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)" -optional = true +optional = false python-versions = ">=3.9" -groups = ["main"] -markers = "extra == \"docs\"" +groups = ["main", "lint"] files = [ {file = "sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331"}, {file = "sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d"}, ] +markers = {main = "extra == \"docs\""} [package.extras] lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] standalone = ["Sphinx (>=5)"] test = ["pytest"] +[[package]] +name = "tabulate" +version = "0.9.0" +description = "Pretty-print tabular data" +optional = false +python-versions = ">=3.7" +groups = ["lint"] +files = [ + {file = "tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f"}, + {file = "tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c"}, +] + +[package.extras] +widechars = ["wcwidth"] + +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +groups = ["lint"] +markers = "python_version < \"3.11\"" +files = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] + [[package]] name = "tomli" version = "2.3.0" description = "A lil' TOML parser" optional = false python-versions = ">=3.8" -groups = ["main", "dev"] +groups = ["main", "dev", "lint"] files = [ {file = "tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45"}, {file = "tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba"}, @@ -949,7 +1719,7 @@ files = [ {file = "tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b"}, {file = "tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549"}, ] -markers = {main = "extra == \"docs\" and python_version < \"3.11\"", dev = "python_full_version <= \"3.11.0a6\""} +markers = {main = "extra == \"docs\" and python_version < \"3.11\"", dev = "python_full_version <= \"3.11.0a6\"", lint = "python_version < \"3.11\""} [[package]] name = "typing-extensions" @@ -957,25 +1727,25 @@ version = "4.15.0" description = "Backported and Experimental Type Hints for Python 3.9+" optional = false python-versions = ">=3.9" -groups = ["dev"] -markers = "python_version < \"3.11\"" +groups = ["dev", "lint"] files = [ {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, ] +markers = {dev = "python_version < \"3.11\""} [[package]] name = "urllib3" version = "2.5.0" description = "HTTP library with thread-safe connection pooling, file post, and more." -optional = true +optional = false python-versions = ">=3.9" -groups = ["main"] -markers = "extra == \"docs\"" +groups = ["main", "lint"] files = [ {file = "urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"}, {file = "urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760"}, ] +markers = {main = "extra == \"docs\""} [package.extras] brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] @@ -987,14 +1757,14 @@ zstd = ["zstandard (>=0.18.0)"] name = "zipp" version = "3.23.0" description = "Backport of pathlib-compatible object wrapper for zip files" -optional = true +optional = false python-versions = ">=3.9" -groups = ["main"] -markers = "extra == \"docs\" and python_version == \"3.9\"" +groups = ["main", "lint"] files = [ {file = "zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e"}, {file = "zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166"}, ] +markers = {main = "extra == \"docs\" and python_version == \"3.9\"", lint = "python_version == \"3.9\""} [package.extras] check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] diff --git a/pyproject.toml b/pyproject.toml index 0b5b86b..704889a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,12 @@ dev = [ "pytest >= 8.4.2", "pytest-cov >= 7.0.0", ] +lint = [ + "docstrfmt >= 1.11.1", + "mypy >= 1.18.2", + "ruff >= 0.6.4", + "sphinx-lint >= 1.0.0", +] [project.optional-dependencies] docs = ["sphinx >= 7.4.7", "sphinx-rtd-theme >= 3.0.2"] @@ -49,14 +55,39 @@ include = [ { path = "test", format = "sdist" }, ] -[tool.poetry.requires-plugins] -poethepoet = ">=0.32" +[tool.poe.tasks.check-docs-links] +help = "Check the documentation for broken URLs" +cmd = "make -C docs linkcheck" + +[tool.poe.tasks.check-format] +help = "Check the code for style issues" +cmd = "ruff format --check --diff" + +[tool.poe.tasks.check-types] +help = "Check the code for typing issues. Accepts mypy options." +cmd = "mypy" [tool.poe.tasks.docs] help = "Build documentation" args = [{ name = "COMMANDS", positional = true, multiple = true, default = "html" }] cmd = "make -C docs $COMMANDS" +[tool.poe.tasks.format] +help = "Format the codebase" +cmd = "ruff format" + +[tool.poe.tasks.format-docs] +help = "Format the documentation" +cmd = "docstrfmt docs *.rst" + +[tool.poe.tasks.lint] +help = "Check the code for linting issues. Accepts ruff options." +cmd = "ruff check" + +[tool.poe.tasks.lint-docs] +help = "Lint the documentation" +shell = "sphinx-lint --enable all --disable default-role $(git ls-files '*.rst')" + [tool.poe.tasks.test] help = "Run tests with pytest" cmd = "pytest $OPTS" @@ -77,3 +108,31 @@ env.OPTS = """ --cov-branch --cov-context=test """ + +[tool.docstrfmt] +line-length = 80 + +[tool.ruff] +target-version = "py39" + +[tool.ruff.lint] +future-annotations = true +select = [ + "E", # pycodestyle + "F", # pyflakes + "G", # flake8-logging-format + "I", # isort + "ISC", # flake8-implicit-str-concat + "N", # pep8-naming + "PT", # flake8-pytest-style + "RUF", # ruff + "UP", # pyupgrade + "TC", # flake8-type-checking + "W", # pycodestyle +] +ignore = [ + "TC006", # no need to quote 'cast's since we use 'from __future__ import annotations' +] + +[tool.ruff.format] +quote-style = "double" From 2bb508ef08a0acfd0c8f8b6f8b48a240d2309740 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= Date: Sat, 27 Dec 2025 14:49:32 +0000 Subject: [PATCH 02/15] Reformat the codebase --- confuse/__init__.py | 15 +- confuse/core.py | 131 +++++---- confuse/exceptions.py | 34 ++- confuse/sources.py | 72 ++--- confuse/templates.py | 210 +++++++------- confuse/util.py | 41 ++- confuse/yaml_util.py | 60 ++-- example/__init__.py | 79 ++--- test/__init__.py | 15 +- test/test_cli.py | 158 +++++----- test/test_dump.py | 93 +++--- test/test_env.py | 312 ++++++++++---------- test/test_paths.py | 147 ++++++---- test/test_utils.py | 67 ++--- test/test_valid.py | 622 ++++++++++++++++++++-------------------- test/test_validation.py | 144 +++++----- test/test_views.py | 248 ++++++++-------- test/test_yaml.py | 81 +++--- 18 files changed, 1296 insertions(+), 1233 deletions(-) diff --git a/confuse/__init__.py b/confuse/__init__.py index b061be4..9076be7 100644 --- a/confuse/__init__.py +++ b/confuse/__init__.py @@ -1,9 +1,8 @@ -"""Painless YAML configuration. -""" +"""Painless YAML configuration.""" -from .exceptions import * # NOQA -from .util import * # NOQA -from .yaml_util import * # NOQA -from .sources import * # NOQA -from .templates import * # NOQA -from .core import * # NOQA +from .exceptions import * # NOQA +from .util import * # NOQA +from .yaml_util import * # NOQA +from .sources import * # NOQA +from .templates import * # NOQA +from .core import * # NOQA diff --git a/confuse/core.py b/confuse/core.py index cde272a..52239d3 100644 --- a/confuse/core.py +++ b/confuse/core.py @@ -13,40 +13,46 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -"""Worry-free YAML configuration files. -""" +"""Worry-free YAML configuration files.""" + from __future__ import annotations __all__ = [ - 'CONFIG_FILENAME', 'DEFAULT_FILENAME', 'ROOT_NAME', 'REDACTED_TOMBSTONE', - 'ConfigView', 'RootView', 'Subview', 'Configuration', 'LazyConfig' + "CONFIG_FILENAME", + "DEFAULT_FILENAME", + "ROOT_NAME", + "REDACTED_TOMBSTONE", + "ConfigView", + "RootView", + "Subview", + "Configuration", + "LazyConfig", ] import errno import os +from collections import OrderedDict from pathlib import Path from typing import Any, Iterable, Sequence, TypeVar + import yaml -from collections import OrderedDict -from . import util -from . import templates -from . import yaml_util +from . import templates, util, yaml_util +from .exceptions import ConfigError, ConfigTypeError, NotFoundError from .sources import ConfigSource, EnvSource, YamlSource -from .exceptions import ConfigTypeError, NotFoundError, ConfigError -CONFIG_FILENAME = 'config.yaml' -DEFAULT_FILENAME = 'config_default.yaml' -ROOT_NAME = 'root' +CONFIG_FILENAME = "config.yaml" +DEFAULT_FILENAME = "config_default.yaml" +ROOT_NAME = "root" -REDACTED_TOMBSTONE = 'REDACTED' +REDACTED_TOMBSTONE = "REDACTED" -R = TypeVar('R') +R = TypeVar("R") # Views and sources. -class ConfigView(): +class ConfigView: """A configuration "view" is a query into a program's configuration data. A view represents a hypothetical location in the configuration tree; to extract the data from the location, a client typically @@ -81,8 +87,7 @@ def first(self): raise NotFoundError("{0} not found".format(self.name)) def exists(self): - """Determine whether the view has a setting in any source. - """ + """Determine whether the view has a setting in any source.""" try: self.first() except NotFoundError: @@ -104,12 +109,11 @@ def set(self, value): raise NotImplementedError def root(self): - """The RootView object from which this view is descended. - """ + """The RootView object from which this view is descended.""" raise NotImplementedError def __repr__(self): - return '<{}: {}>'.format(self.__class__.__name__, self.name) + return "<{}: {}>".format(self.__class__.__name__, self.name) def __iter__(self): """Iterate over the keys of a dictionary view or the *subviews* @@ -129,7 +133,7 @@ def __iter__(self): except ConfigTypeError: item, _ = self.first() raise ConfigTypeError( - '{0} must be a dictionary or a list, not {1}'.format( + "{0} must be a dictionary or a list, not {1}".format( self.name, type(item).__name__ ) ) @@ -163,7 +167,7 @@ def set_args(self, namespace, dots=False): {'foo': {'bar': 'car'}} :type dots: bool """ - self.set(util.build_dict(namespace, sep='.' if dots else '')) + self.set(util.build_dict(namespace, sep="." if dots else "")) # Magical conversions. These special methods make it possible to use # View objects somewhat transparently in certain circumstances. For @@ -171,13 +175,11 @@ def set_args(self, namespace, dots=False): # just say ``bool(view)`` or use ``view`` in a conditional. def __str__(self): - """Get the value for this view as a bytestring. - """ + """Get the value for this view as a bytestring.""" return str(self.get()) def __bool__(self): - """Gets the value for this view as a bool. - """ + """Gets the value for this view as a bool.""" return bool(self.get()) # Dictionary emulation methods. @@ -198,9 +200,7 @@ def keys(self): cur_keys = dic.keys() except AttributeError: raise ConfigTypeError( - '{0} must be a dict, not {1}'.format( - self.name, type(dic).__name__ - ) + "{0} must be a dict, not {1}".format(self.name, type(dic).__name__) ) for key in cur_keys: @@ -238,7 +238,7 @@ def sequence(self): return if not isinstance(collection, (list, tuple)): raise ConfigTypeError( - '{0} must be a list, not {1}'.format( + "{0} must be a list, not {1}".format( self.name, type(collection).__name__ ) ) @@ -259,7 +259,7 @@ def all_contents(self): it = iter(collection) except TypeError: raise ConfigTypeError( - '{0} must be an iterable, not {1}'.format( + "{0} must be an iterable, not {1}".format( self.name, type(collection).__name__ ) ) @@ -305,13 +305,11 @@ def get(self, template=templates.REQUIRED) -> Any: # Shortcuts for common templates. def as_filename(self) -> str: - """Get the value as a path. Equivalent to `get(Filename())`. - """ + """Get the value as a path. Equivalent to `get(Filename())`.""" return self.get(templates.Filename()) def as_path(self) -> Path: - """Get the value as a `pathlib.Path` object. Equivalent to `get(Path())`. - """ + """Get the value as a `pathlib.Path` object. Equivalent to `get(Path())`.""" return self.get(templates.Path()) def as_choice(self, choices: Iterable[R]) -> R: @@ -373,8 +371,7 @@ def set_redaction(self, path, flag): raise NotImplementedError() def get_redactions(self): - """Get the set of currently-redacted sub-key-paths at this view. - """ + """Get the set of currently-redacted sub-key-paths at this view.""" raise NotImplementedError() @@ -382,6 +379,7 @@ class RootView(ConfigView): """The base of a view hierarchy. This view keeps track of the sources that may be accessed by subviews. """ + def __init__(self, sources): """Create a configuration hierarchy for a list of sources. At least one source must be provided. The first source in the list @@ -422,23 +420,23 @@ def get_redactions(self): class Subview(ConfigView): """A subview accessed via a subscript of a parent view.""" + def __init__(self, parent, key): - """Make a subview of a parent view for a given subscript key. - """ + """Make a subview of a parent view for a given subscript key.""" self.parent = parent self.key = key # Choose a human-readable name for this view. if isinstance(self.parent, RootView): - self.name = '' + self.name = "" else: self.name = self.parent.name if not isinstance(self.key, int): - self.name += '.' + self.name += "." if isinstance(self.key, int): - self.name += '#{0}'.format(self.key) + self.name += "#{0}".format(self.key) elif isinstance(self.key, bytes): - self.name += self.key.decode('utf-8') + self.name += self.key.decode("utf-8") elif isinstance(self.key, str): self.name += self.key else: @@ -476,15 +474,16 @@ def set_redaction(self, path, flag): self.parent.set_redaction((self.key,) + path, flag) def get_redactions(self): - return (kp[1:] for kp in self.parent.get_redactions() - if kp and kp[0] == self.key) + return ( + kp[1:] for kp in self.parent.get_redactions() if kp and kp[0] == self.key + ) + # Main interface. class Configuration(RootView): - def __init__(self, appname, modname=None, read=True, - loader=yaml_util.Loader): + def __init__(self, appname, modname=None, read=True, loader=yaml_util.Loader): """Create a configuration object by reading the automatically-discovered config files for the application for a given name. If `modname` is specified, it should be the import @@ -507,7 +506,7 @@ def __init__(self, appname, modname=None, read=True, else: self._package_path = None - self._env_var = '{0}DIR'.format(self.appname.upper()) + self._env_var = "{0}DIR".format(self.appname.upper()) if read: self.read() @@ -535,8 +534,11 @@ def _add_default_source(self): if self.modname: if self._package_path: filename = os.path.join(self._package_path, DEFAULT_FILENAME) - self.add(YamlSource(filename, loader=self.loader, - optional=True, default=True)) + self.add( + YamlSource( + filename, loader=self.loader, optional=True, default=True + ) + ) def read(self, user=True, defaults=True): """Find and read the files for this configuration and set them @@ -565,9 +567,7 @@ def config_dir(self): appdir = os.environ[self._env_var] appdir = os.path.abspath(os.path.expanduser(appdir)) if os.path.isfile(appdir): - raise ConfigError('{0} must be a directory'.format( - self._env_var - )) + raise ConfigError("{0} must be a directory".format(self._env_var)) else: # Search platform-specific locations. If no config file is @@ -599,10 +599,11 @@ def set_file(self, filename, base_for_paths=False): path values stored in the YAML file. Otherwise, by default, the directory returned by `config_dir()` will be used as the base. """ - self.set(YamlSource(filename, base_for_paths=base_for_paths, - loader=self.loader)) + self.set( + YamlSource(filename, base_for_paths=base_for_paths, loader=self.loader) + ) - def set_env(self, prefix=None, sep='__'): + def set_env(self, prefix=None, sep="__"): """Create a configuration overlay at the highest priority from environment variables. @@ -621,7 +622,7 @@ def set_env(self, prefix=None, sep='__'): :param sep: Separator within variable names to define nested keys. """ if prefix is None: - prefix = '{0}_'.format(self.appname.upper()) + prefix = "{0}_".format(self.appname.upper()) self.set(EnvSource(prefix, sep=sep, loader=self.loader)) def dump(self, full=True, redact=False): @@ -645,9 +646,13 @@ def dump(self, full=True, redact=False): temp_root.redactions = self.redactions out_dict = temp_root.flatten(redact=redact) - yaml_out = yaml.dump(out_dict, Dumper=yaml_util.Dumper, - default_flow_style=None, indent=4, - width=1000) + yaml_out = yaml.dump( + out_dict, + Dumper=yaml_util.Dumper, + default_flow_style=None, + indent=4, + width=1000, + ) # Restore comments to the YAML text. default_source = None @@ -656,10 +661,11 @@ def dump(self, full=True, redact=False): default_source = source break if default_source and default_source.filename: - with open(default_source.filename, 'rb') as fp: + with open(default_source.filename, "rb") as fp: default_data = fp.read() yaml_out = yaml_util.restore_yaml_comments( - yaml_out, default_data.decode('utf-8')) + yaml_out, default_data.decode("utf-8") + ) return yaml_out @@ -680,6 +686,7 @@ class LazyConfig(Configuration): accessed. This is appropriate for using as a global config object at the module level. """ + def __init__(self, appname, modname=None): super().__init__(appname, modname, False) self._materialized = False # Have we read the files yet? diff --git a/confuse/exceptions.py b/confuse/exceptions.py index 8933463..3b46696 100644 --- a/confuse/exceptions.py +++ b/confuse/exceptions.py @@ -1,8 +1,13 @@ import yaml __all__ = [ - 'ConfigError', 'NotFoundError', 'ConfigValueError', 'ConfigTypeError', - 'ConfigTemplateError', 'ConfigReadError'] + "ConfigError", + "NotFoundError", + "ConfigValueError", + "ConfigTypeError", + "ConfigTemplateError", + "ConfigReadError", +] YAML_TAB_PROBLEM = "found character '\\t' that cannot start any token" @@ -10,13 +15,11 @@ class ConfigError(Exception): - """Base class for exceptions raised when querying a configuration. - """ + """Base class for exceptions raised when querying a configuration.""" class NotFoundError(ConfigError): - """A requested value could not be found in the configuration trees. - """ + """A requested value could not be found in the configuration trees.""" class ConfigValueError(ConfigError): @@ -24,31 +27,32 @@ class ConfigValueError(ConfigError): class ConfigTypeError(ConfigValueError): - """The value in the configuration did not match the expected type. - """ + """The value in the configuration did not match the expected type.""" class ConfigTemplateError(ConfigError): - """Base class for exceptions raised because of an invalid template. - """ + """Base class for exceptions raised because of an invalid template.""" class ConfigReadError(ConfigError): """A configuration source could not be read.""" + def __init__(self, name, reason=None): self.name = name self.reason = reason - message = '{0} could not be read'.format(name) - if (isinstance(reason, yaml.scanner.ScannerError) - and reason.problem == YAML_TAB_PROBLEM): + message = "{0} could not be read".format(name) + if ( + isinstance(reason, yaml.scanner.ScannerError) + and reason.problem == YAML_TAB_PROBLEM + ): # Special-case error message for tab indentation in YAML markup. - message += ': found tab character at line {0}, column {1}'.format( + message += ": found tab character at line {0}, column {1}".format( reason.problem_mark.line + 1, reason.problem_mark.column + 1, ) elif reason: # Generic error message uses exception's message. - message += ': {0}'.format(reason) + message += ": {0}".format(reason) super().__init__(message) diff --git a/confuse/sources.py b/confuse/sources.py index ab297cf..a665aec 100644 --- a/confuse/sources.py +++ b/confuse/sources.py @@ -1,14 +1,15 @@ -from .util import build_dict -from . import yaml_util import os +from . import yaml_util +from .util import build_dict + class ConfigSource(dict): """A dictionary augmented with metadata about the source of the configuration. """ - def __init__(self, value, filename=None, default=False, - base_for_paths=False): + + def __init__(self, value, filename=None, default=False, base_for_paths=False): """Create a configuration source from a dictionary. :param filename: The file with the data for this configuration source. @@ -26,13 +27,13 @@ def __init__(self, value, filename=None, default=False, """ super().__init__(value) if filename is not None and not isinstance(filename, str): - raise TypeError('filename must be a string or None') + raise TypeError("filename must be a string or None") self.filename = filename self.default = default self.base_for_paths = base_for_paths if filename is not None else False def __repr__(self): - return 'ConfigSource({0!r}, {1!r}, {2!r}, {3!r})'.format( + return "ConfigSource({0!r}, {1!r}, {2!r}, {3!r})".format( super(), self.filename, self.default, @@ -50,15 +51,20 @@ def of(cls, value): elif isinstance(value, dict): return ConfigSource(value) else: - raise TypeError('source value must be a dict') + raise TypeError("source value must be a dict") class YamlSource(ConfigSource): - """A configuration data source that reads from a YAML file. - """ - - def __init__(self, filename=None, default=False, base_for_paths=False, - optional=False, loader=yaml_util.Loader): + """A configuration data source that reads from a YAML file.""" + + def __init__( + self, + filename=None, + default=False, + base_for_paths=False, + optional=False, + loader=yaml_util.Loader, + ): """Create a YAML data source by reading data from a file. May raise a `ConfigReadError`. However, if `optional` is @@ -73,21 +79,26 @@ def __init__(self, filename=None, default=False, base_for_paths=False, self.load() def load(self): - """Load YAML data from the source's filename. - """ + """Load YAML data from the source's filename.""" if self.optional and not os.path.isfile(self.filename): value = {} else: - value = yaml_util.load_yaml(self.filename, - loader=self.loader) or {} + value = yaml_util.load_yaml(self.filename, loader=self.loader) or {} self.update(value) class EnvSource(ConfigSource): - """A configuration data source loaded from environment variables. - """ - def __init__(self, prefix, sep='__', lower=True, handle_lists=True, - parse_yaml_docs=False, loader=yaml_util.Loader): + """A configuration data source loaded from environment variables.""" + + def __init__( + self, + prefix, + sep="__", + lower=True, + handle_lists=True, + parse_yaml_docs=False, + loader=yaml_util.Loader, + ): """Create a configuration source from the environment. :param prefix: The prefix used to identify the environment variables @@ -108,8 +119,7 @@ def __init__(self, prefix, sep='__', lower=True, handle_lists=True, :param loader: PyYAML Loader class to use to parse YAML values. """ - super().__init__({}, filename=None, default=False, - base_for_paths=False) + super().__init__({}, filename=None, default=False, base_for_paths=False) self.prefix = prefix self.sep = sep self.lower = lower @@ -119,13 +129,12 @@ def __init__(self, prefix, sep='__', lower=True, handle_lists=True, self.load() def load(self): - """Load configuration data from the environment. - """ + """Load configuration data from the environment.""" # Read config variables with prefix from the environment. config_vars = {} for var, value in os.environ.items(): if var.startswith(self.prefix): - key = var[len(self.prefix):] + key = var[len(self.prefix) :] if self.lower: key = key.lower() if self.parse_yaml_docs: @@ -133,16 +142,15 @@ def load(self): # string representations of dicts and lists into the # appropriate object (ie, '{foo: bar}' to {'foo': 'bar'}). # Will raise a ConfigReadError if YAML parsing fails. - value = yaml_util.load_yaml_string(value, - 'env variable ' + var, - loader=self.loader) + value = yaml_util.load_yaml_string( + value, "env variable " + var, loader=self.loader + ) else: # Parse the value as a YAML scalar so that values are type # converted using the same rules as the YAML Loader (ie, # numeric string to int/float, 'true' to True, etc.). Will # not raise a ConfigReadError. - value = yaml_util.parse_as_scalar(value, - loader=self.loader) + value = yaml_util.parse_as_scalar(value, loader=self.loader) config_vars[key] = value if self.sep: # Build a nested dict, keeping keys with `None` values to allow @@ -169,13 +177,13 @@ def _convert_dict_lists(cls, obj): try: # Convert the keys to integers, mapping the ints back to the keys int_to_key = {int(k): k for k in obj.keys()} - except (ValueError): + except ValueError: # Not all of the keys represent integers return obj try: # For the integers from 0 to the length of the dict, try to create # a list from the dict values using the integer to key mapping return [obj[int_to_key[i]] for i in range(len(obj))] - except (KeyError): + except KeyError: # At least one integer within the range is not a key of the dict return obj diff --git a/confuse/templates.py b/confuse/templates.py index 377bd97..7ff1503 100644 --- a/confuse/templates.py +++ b/confuse/templates.py @@ -1,12 +1,10 @@ -import os -import re import enum +import os import pathlib +import re from collections import abc -from . import util -from . import exceptions - +from . import exceptions, util REQUIRED = object() """A sentinel indicating that there is no default value and an exception @@ -14,7 +12,7 @@ """ -class Template(): +class Template: """A value template for configuration fields. The template works like a type and instructs Confuse about how to @@ -22,6 +20,7 @@ class Template(): providing a default value, and validating for errors. For example, a filepath type might expand tildes and check that the file exists. """ + def __init__(self, default=REQUIRED): """Create a template with a given default value. @@ -52,12 +51,12 @@ def value(self, view, template=None): # Get default value, or raise if required. return self.get_default_value(view.name) - def get_default_value(self, key_name='default'): + def get_default_value(self, key_name="default"): """Get the default value to return when the value is missing. May raise a `NotFoundError` if the value is required. """ - if not hasattr(self, 'default') or self.default is REQUIRED: + if not hasattr(self, "default") or self.default is REQUIRED: # The value is required. A missing value is an error. raise exceptions.NotFoundError("{} not found".format(key_name)) # The value is not required. @@ -82,44 +81,40 @@ def fail(self, message, view, type_error=False): specific exception is raised. """ exc_class = ( - exceptions.ConfigTypeError if type_error - else exceptions.ConfigValueError) - raise exc_class('{0}: {1}'.format(view.name, message)) + exceptions.ConfigTypeError if type_error else exceptions.ConfigValueError + ) + raise exc_class("{0}: {1}".format(view.name, message)) def __repr__(self): - return '{0}({1})'.format( + return "{0}({1})".format( type(self).__name__, - '' if self.default is REQUIRED else repr(self.default), + "" if self.default is REQUIRED else repr(self.default), ) class Integer(Template): - """An integer configuration value template. - """ + """An integer configuration value template.""" + def convert(self, value, view): - """Check that the value is an integer. Floats are rounded. - """ + """Check that the value is an integer. Floats are rounded.""" if isinstance(value, int): return value elif isinstance(value, float): return int(value) else: - self.fail('must be a number', view, True) + self.fail("must be a number", view, True) class Number(Template): - """A numeric type: either an integer or a floating-point number. - """ + """A numeric type: either an integer or a floating-point number.""" + def convert(self, value, view): - """Check that the value is an int or a float. - """ + """Check that the value is an int or a float.""" if isinstance(value, (int, float)): return value else: self.fail( - 'must be numeric, not {0}'.format(type(value).__name__), - view, - True + "must be numeric, not {0}".format(type(value).__name__), view, True ) @@ -127,6 +122,7 @@ class MappingTemplate(Template): """A template that uses a dictionary to specify other types for the values for a set of keys and produce a validated `AttrDict`. """ + def __init__(self, mapping): """Create a template according to a dict (mapping). The mapping's values should themselves either be Types or @@ -147,13 +143,14 @@ def value(self, view, template=None): return out def __repr__(self): - return 'MappingTemplate({0})'.format(repr(self.subtemplates)) + return "MappingTemplate({0})".format(repr(self.subtemplates)) class Sequence(Template): """A template used to validate lists of similar items, based on a given subtemplate. """ + def __init__(self, subtemplate): """Create a template for a list with items validated on a given subtemplate. @@ -161,15 +158,14 @@ def __init__(self, subtemplate): self.subtemplate = as_template(subtemplate) def value(self, view, template=None): - """Get a list of items validated against the template. - """ + """Get a list of items validated against the template.""" out = [] for item in view.sequence(): out.append(self.subtemplate.value(item, self)) return out def __repr__(self): - return 'Sequence({0})'.format(repr(self.subtemplate)) + return "Sequence({0})".format(repr(self.subtemplate)) class MappingValues(Template): @@ -180,6 +176,7 @@ class MappingValues(Template): must pass validation by the subtemplate. Similar to the Sequence template but for mappings. """ + def __init__(self, subtemplate): """Create a template for a mapping with variable keys and item values validated on a given subtemplate. @@ -196,12 +193,12 @@ def value(self, view, template=None): return out def __repr__(self): - return 'MappingValues({0})'.format(repr(self.subtemplate)) + return "MappingValues({0})".format(repr(self.subtemplate)) class String(Template): - """A string configuration value template. - """ + """A string configuration value template.""" + def __init__(self, default=REQUIRED, pattern=None, expand_vars=False): """Create a template with the added optional `pattern` argument, a regular expression string that the value should match. @@ -219,21 +216,17 @@ def __repr__(self): args.append(repr(self.default)) if self.pattern is not None: - args.append('pattern=' + repr(self.pattern)) + args.append("pattern=" + repr(self.pattern)) - return 'String({0})'.format(', '.join(args)) + return "String({0})".format(", ".join(args)) def convert(self, value, view): - """Check that the value is a string and matches the pattern. - """ + """Check that the value is a string and matches the pattern.""" if not isinstance(value, str): - self.fail('must be a string', view, True) + self.fail("must be a string", view, True) if self.pattern and not self.regex.match(value): - self.fail( - "must match the pattern {0}".format(self.pattern), - view - ) + self.fail("must match the pattern {0}".format(self.pattern), view) if self.expand_vars: return os.path.expandvars(value) @@ -247,6 +240,7 @@ class Choice(Template): Sequences, dictionaries and :class:`Enum` types are supported, see :meth:`__init__` for usage. """ + def __init__(self, choices, default=REQUIRED): """Create a template that validates any of the values from the iterable `choices`. @@ -264,24 +258,21 @@ def convert(self, value, view): """Ensure that the value is among the choices (and remap if the choices are a mapping). """ - if (isinstance(self.choices, type) - and issubclass(self.choices, enum.Enum)): + if isinstance(self.choices, type) and issubclass(self.choices, enum.Enum): try: return self.choices(value) except ValueError: self.fail( - 'must be one of {0!r}, not {1!r}'.format( + "must be one of {0!r}, not {1!r}".format( [c.value for c in self.choices], value ), - view + view, ) if value not in self.choices: self.fail( - 'must be one of {0!r}, not {1!r}'.format( - list(self.choices), value - ), - view + "must be one of {0!r}, not {1!r}".format(list(self.choices), value), + view, ) if isinstance(self.choices, abc.Mapping): @@ -290,12 +281,12 @@ def convert(self, value, view): return value def __repr__(self): - return 'Choice({0!r})'.format(self.choices) + return "Choice({0!r})".format(self.choices) class OneOf(Template): - """A template that permits values complying to one of the given templates. - """ + """A template that permits values complying to one of the given templates.""" + def __init__(self, allowed, default=REQUIRED): super().__init__(default) self.allowed = list(allowed) @@ -304,20 +295,19 @@ def __repr__(self): args = [] if self.allowed is not None: - args.append('allowed=' + repr(self.allowed)) + args.append("allowed=" + repr(self.allowed)) if self.default is not REQUIRED: args.append(repr(self.default)) - return 'OneOf({0})'.format(', '.join(args)) + return "OneOf({0})".format(", ".join(args)) def value(self, view, template): self.template = template return super().value(view, template) def convert(self, value, view): - """Ensure that the value follows at least one template. - """ + """Ensure that the value follows at least one template.""" is_mapping = isinstance(self.template, MappingTemplate) for candidate in self.allowed: @@ -335,10 +325,7 @@ def convert(self, value, view): raise exceptions.ConfigTemplateError(exc) self.fail( - 'must be one of {0}, not {1}'.format( - repr(self.allowed), repr(value) - ), - view + "must be one of {0}, not {1}".format(repr(self.allowed), repr(value)), view ) @@ -348,6 +335,7 @@ class StrSeq(Template): Validates both actual YAML string lists and single strings. Strings can optionally be split on whitespace. """ + def __init__(self, split=True, default=REQUIRED): """Create a new template. @@ -362,13 +350,13 @@ def _convert_value(self, x, view): if isinstance(x, str): return x elif isinstance(x, bytes): - return x.decode('utf-8', 'ignore') + return x.decode("utf-8", "ignore") else: - self.fail('must be a list of strings', view, True) + self.fail("must be a list of strings", view, True) def convert(self, value, view): if isinstance(value, bytes): - value = value.decode('utf-8', 'ignore') + value = value.decode("utf-8", "ignore") if isinstance(value, str): if self.split: @@ -379,8 +367,7 @@ def convert(self, value, view): try: value = list(value) except TypeError: - self.fail('must be a whitespace-separated string or a list', - view, True) + self.fail("must be a whitespace-separated string or a list", view, True) return [self._convert_value(v, view) for v in value] @@ -409,25 +396,23 @@ def __init__(self, default_value=None): def _convert_value(self, x, view): try: - return (super()._convert_value(x, view), - self.default_value) + return (super()._convert_value(x, view), self.default_value) except exceptions.ConfigTypeError: if isinstance(x, abc.Mapping): if len(x) != 1: - self.fail('must be a single-element mapping', view, True) + self.fail("must be a single-element mapping", view, True) k, v = util.iter_first(x.items()) elif isinstance(x, abc.Sequence): if len(x) != 2: - self.fail('must be a two-element list', view, True) + self.fail("must be a two-element list", view, True) k, v = x else: # Is this even possible? -> Likely, if some !directive cause # YAML to parse this to some custom type. - self.fail('must be a single string, mapping, or a list' - '' + str(x), - view, True) - return (super()._convert_value(k, view), - super()._convert_value(v, view)) + self.fail( + "must be a single string, mapping, or a list" + str(x), view, True + ) + return (super()._convert_value(k, view), super()._convert_value(v, view)) class Filename(Template): @@ -444,8 +429,15 @@ class Filename(Template): without a file are relative to the current working directory. This helps attain the expected behavior when using command-line options. """ - def __init__(self, default=REQUIRED, cwd=None, relative_to=None, - in_app_dir=False, in_source_dir=False): + + def __init__( + self, + default=REQUIRED, + cwd=None, + relative_to=None, + in_app_dir=False, + in_source_dir=False, + ): """`relative_to` is the name of a sibling value that is being validated at the same time. @@ -470,38 +462,38 @@ def __repr__(self): args.append(repr(self.default)) if self.cwd is not None: - args.append('cwd=' + repr(self.cwd)) + args.append("cwd=" + repr(self.cwd)) if self.relative_to is not None: - args.append('relative_to=' + repr(self.relative_to)) + args.append("relative_to=" + repr(self.relative_to)) if self.in_app_dir: - args.append('in_app_dir=True') + args.append("in_app_dir=True") if self.in_source_dir: - args.append('in_source_dir=True') + args.append("in_source_dir=True") - return 'Filename({0})'.format(', '.join(args)) + return "Filename({0})".format(", ".join(args)) def resolve_relative_to(self, view, template): if not isinstance(template, (abc.Mapping, MappingTemplate)): # disallow config.get(Filename(relative_to='foo')) raise exceptions.ConfigTemplateError( - 'relative_to may only be used when getting multiple values.' + "relative_to may only be used when getting multiple values." ) elif self.relative_to == view.key: raise exceptions.ConfigTemplateError( - '{0} is relative to itself'.format(view.name) + "{0} is relative to itself".format(view.name) ) elif self.relative_to not in view.parent.keys(): # self.relative_to is not in the config self.fail( - ( - 'needs sibling value "{0}" to expand relative path' - ).format(self.relative_to), - view + ('needs sibling value "{0}" to expand relative path').format( + self.relative_to + ), + view, ) old_template = {} @@ -520,14 +512,18 @@ def resolve_relative_to(self, view, template): except KeyError: if next_relative in template.subtemplates: # we encountered this config key previously - raise exceptions.ConfigTemplateError(( - '{0} and {1} are recursively relative' - ).format(view.name, self.relative_to)) + raise exceptions.ConfigTemplateError( + ("{0} and {1} are recursively relative").format( + view.name, self.relative_to + ) + ) else: - raise exceptions.ConfigTemplateError(( - 'missing template for {0}, needed to expand {1}\'s' - 'relative path' - ).format(self.relative_to, view.name)) + raise exceptions.ConfigTemplateError( + ( + "missing template for {0}, needed to expand {1}'s" + "relative path" + ).format(self.relative_to, view.name) + ) next_template.subtemplates[next_relative] = rel_to_template next_relative = rel_to_template.relative_to @@ -542,9 +538,7 @@ def value(self, view, template=None): if not isinstance(path, str): self.fail( - 'must be a filename, not {0}'.format(type(path).__name__), - view, - True + "must be a filename, not {0}".format(type(path).__name__), view, True ) path = os.path.expanduser(str(path)) @@ -559,8 +553,9 @@ def value(self, view, template=None): path, ) - elif ((source.filename and self.in_source_dir) - or (source.base_for_paths and not self.in_app_dir)): + elif (source.filename and self.in_source_dir) or ( + source.base_for_paths and not self.in_app_dir + ): # relative to the directory the source file is in. path = os.path.join(os.path.dirname(source.filename), path) @@ -577,11 +572,13 @@ class Path(Filename): Filenames are parsed equivalent to the `Filename` template and then converted to `pathlib.Path` objects. """ + def value(self, view, template=None): value = super().value(view, template) if value is None: return import pathlib + return pathlib.Path(value) @@ -614,7 +611,7 @@ def value(self, view, template=None): # Value is missing but not required return self.default # Value must be present even though it can be null. Raise an error. - raise exceptions.NotFoundError('{} not found'.format(view.name)) + raise exceptions.NotFoundError("{} not found".format(view.name)) if value is None: # None (ie, null) is always a valid value @@ -622,7 +619,7 @@ def value(self, view, template=None): return self.subtemplate.value(view, self) def __repr__(self): - return 'Optional({0}, {1}, allow_missing={2})'.format( + return "Optional({0}, {1}, allow_missing={2})".format( repr(self.subtemplate), repr(self.default), self.allow_missing, @@ -633,6 +630,7 @@ class TypeTemplate(Template): """A simple template that checks that a value is an instance of a desired Python type. """ + def __init__(self, typ, default=REQUIRED): """Create a template that checks that the value is an instance of `typ`. @@ -643,12 +641,12 @@ def __init__(self, typ, default=REQUIRED): def convert(self, value, view): if not isinstance(value, self.typ): self.fail( - 'must be a {0}, not {1}'.format( + "must be a {0}, not {1}".format( self.typ.__name__, type(value).__name__, ), view, - True + True, ) return value @@ -657,6 +655,7 @@ class AttrDict(dict): """A `dict` subclass that can be accessed via attributes (dot notation) for convenience. """ + def __getattr__(self, key): if key in self: return self[key] @@ -668,8 +667,7 @@ def __setattr__(self, key, value): def as_template(value): - """Convert a simple "shorthand" Python value to a `Template`. - """ + """Convert a simple "shorthand" Python value to a `Template`.""" if isinstance(value, Template): # If it's already a Template, pass it through. return value @@ -708,4 +706,4 @@ def as_template(value): elif isinstance(value, type): return TypeTemplate(value) else: - raise ValueError('cannot convert to template: {0!r}'.format(value)) + raise ValueError("cannot convert to template: {0!r}".format(value)) diff --git a/confuse/util.py b/confuse/util.py index a3f6e62..bcfb909 100644 --- a/confuse/util.py +++ b/confuse/util.py @@ -1,15 +1,14 @@ -import importlib.util -import os -import sys import argparse +import importlib.util import optparse +import os import platform +import sys - -UNIX_DIR_FALLBACK = '~/.config' -WINDOWS_DIR_VAR = 'APPDATA' -WINDOWS_DIR_FALLBACK = '~\\AppData\\Roaming' -MAC_DIR = '~/Library/Application Support' +UNIX_DIR_FALLBACK = "~/.config" +WINDOWS_DIR_VAR = "APPDATA" +WINDOWS_DIR_FALLBACK = "~\\AppData\\Roaming" +MAC_DIR = "~/Library/Application Support" def iter_first(sequence): @@ -38,7 +37,7 @@ def namespace_to_dict(obj): return obj -def build_dict(obj, sep='', keep_none=False): +def build_dict(obj, sep="", keep_none=False): """Recursively builds a dictionary from an argparse.Namespace, optparse.Values, or dict object. @@ -90,8 +89,7 @@ def build_dict(obj, sep='', keep_none=False): # Build the dict tree if needed and change where # we're saving to for child_key in split: - if child_key in save_to and \ - isinstance(save_to[child_key], dict): + if child_key in save_to and isinstance(save_to[child_key], dict): save_to = save_to[child_key] else: # Clobber or create @@ -109,6 +107,7 @@ def build_dict(obj, sep='', keep_none=False): # Config file paths, including platform-specific paths and in-package # defaults. + def find_package_path(name): """Returns the path to the package containing the named module or None if the path could not be identified (e.g., if @@ -122,10 +121,10 @@ def find_package_path(name): return None loader = spec.loader - if loader is None or name == '__main__': + if loader is None or name == "__main__": return None - if hasattr(loader, 'get_filename'): + if hasattr(loader, "get_filename"): filepath = loader.get_filename(name) else: # Fall back to importing the specified module. @@ -140,13 +139,13 @@ def xdg_config_dirs(): and XDG_CONFIG_HOME environment varibables if they exist """ paths = [] - if 'XDG_CONFIG_HOME' in os.environ: - paths.append(os.environ['XDG_CONFIG_HOME']) - if 'XDG_CONFIG_DIRS' in os.environ: - paths.extend(os.environ['XDG_CONFIG_DIRS'].split(':')) + if "XDG_CONFIG_HOME" in os.environ: + paths.append(os.environ["XDG_CONFIG_HOME"]) + if "XDG_CONFIG_DIRS" in os.environ: + paths.extend(os.environ["XDG_CONFIG_DIRS"].split(":")) else: - paths.append('/etc/xdg') - paths.append('/etc') + paths.append("/etc/xdg") + paths.append("/etc") return paths @@ -160,12 +159,12 @@ def config_dirs(): """ paths = [] - if platform.system() == 'Darwin': + if platform.system() == "Darwin": paths.append(UNIX_DIR_FALLBACK) paths.append(MAC_DIR) paths.extend(xdg_config_dirs()) - elif platform.system() == 'Windows': + elif platform.system() == "Windows": paths.append(WINDOWS_DIR_FALLBACK) if WINDOWS_DIR_VAR in os.environ: paths.append(os.environ[WINDOWS_DIR_VAR]) diff --git a/confuse/yaml_util.py b/confuse/yaml_util.py index 48d2252..cda6189 100644 --- a/confuse/yaml_util.py +++ b/confuse/yaml_util.py @@ -1,5 +1,7 @@ from collections import OrderedDict + import yaml + from .exceptions import ConfigReadError # YAML loading. @@ -13,6 +15,7 @@ class Loader(yaml.SafeLoader): - All maps are OrderedDicts. - Strings can begin with % without quotation. """ + # All strings should be Unicode objects, regardless of contents. def _construct_unicode(self, node): return self.construct_scalar(node) @@ -30,9 +33,10 @@ def construct_mapping(self, node, deep=False): self.flatten_mapping(node) else: raise yaml.constructor.ConstructorError( - None, None, - 'expected a mapping node, but found %s' % node.id, - node.start_mark + None, + None, + "expected a mapping node, but found %s" % node.id, + node.start_mark, ) mapping = OrderedDict() @@ -42,9 +46,10 @@ def construct_mapping(self, node, deep=False): hash(key) except TypeError as exc: raise yaml.constructor.ConstructorError( - 'while constructing a mapping', - node.start_mark, 'found unacceptable key (%s)' % exc, - key_node.start_mark + "while constructing a mapping", + node.start_mark, + "found unacceptable key (%s)" % exc, + key_node.start_mark, ) value = self.construct_object(value_node, deep=deep) mapping[key] = value @@ -53,7 +58,7 @@ def construct_mapping(self, node, deep=False): # Allow bare strings to begin with %. Directives are still detected. def check_plain(self): plain = super().check_plain() - return plain or self.peek() == '%' + return plain or self.peek() == "%" @staticmethod def add_constructors(loader): @@ -61,12 +66,9 @@ def add_constructors(loader): and maps. Call this method on a custom Loader class to make it behave like Confuse's own Loader """ - loader.add_constructor('tag:yaml.org,2002:str', - Loader._construct_unicode) - loader.add_constructor('tag:yaml.org,2002:map', - Loader.construct_yaml_map) - loader.add_constructor('tag:yaml.org,2002:omap', - Loader.construct_yaml_map) + loader.add_constructor("tag:yaml.org,2002:str", Loader._construct_unicode) + loader.add_constructor("tag:yaml.org,2002:map", Loader.construct_yaml_map) + loader.add_constructor("tag:yaml.org,2002:omap", Loader.construct_yaml_map) Loader.add_constructors(Loader) @@ -80,7 +82,7 @@ def load_yaml(filename, loader=Loader): extra constructors. """ try: - with open(filename, 'rb') as f: + with open(filename, "rb") as f: return yaml.load(f, Loader=loader) except (IOError, yaml.error.YAMLError) as exc: raise ConfigReadError(filename, exc) @@ -119,7 +121,7 @@ def parse_as_scalar(value, loader=Loader): if not isinstance(value, str): return value try: - loader = loader('') + loader = loader("") tag = loader.resolve(yaml.ScalarNode, value, (True, False)) node = yaml.ScalarNode(tag, value) return loader.construct_object(node) @@ -130,10 +132,12 @@ def parse_as_scalar(value, loader=Loader): # YAML dumping. + class Dumper(yaml.SafeDumper): """A PyYAML Dumper that represents OrderedDicts as ordinary mappings (in order, of course). """ + # From http://pyyaml.org/attachment/ticket/161/use_ordered_dict.py def represent_mapping(self, tag, mapping, flow_style=None): value = [] @@ -141,16 +145,14 @@ def represent_mapping(self, tag, mapping, flow_style=None): if self.alias_key is not None: self.represented_objects[self.alias_key] = node best_style = False - if hasattr(mapping, 'items'): + if hasattr(mapping, "items"): mapping = list(mapping.items()) for item_key, item_value in mapping: node_key = self.represent_data(item_key) node_value = self.represent_data(item_value) - if not (isinstance(node_key, yaml.ScalarNode) - and not node_key.style): + if not (isinstance(node_key, yaml.ScalarNode) and not node_key.style): best_style = False - if not (isinstance(node_value, yaml.ScalarNode) - and not node_value.style): + if not (isinstance(node_value, yaml.ScalarNode) and not node_value.style): best_style = False value.append((node_key, node_value)) if flow_style is None: @@ -173,18 +175,16 @@ def represent_list(self, data): return node def represent_bool(self, data): - """Represent bool as 'yes' or 'no' instead of 'true' or 'false'. - """ + """Represent bool as 'yes' or 'no' instead of 'true' or 'false'.""" if data: - value = 'yes' + value = "yes" else: - value = 'no' - return self.represent_scalar('tag:yaml.org,2002:bool', value) + value = "no" + return self.represent_scalar("tag:yaml.org,2002:bool", value) def represent_none(self, data): - """Represent a None value with nothing instead of 'none'. - """ - return self.represent_scalar('tag:yaml.org,2002:null', '') + """Represent a None value with nothing instead of 'none'.""" + return self.represent_scalar("tag:yaml.org,2002:null", "") Dumper.add_representer(OrderedDict, Dumper.represent_dict) @@ -213,12 +213,12 @@ def restore_yaml_comments(data, default_data): if line and not line.startswith("#"): break comment += "{0}\n".format(line) - key = line.split(':')[0].strip() + key = line.split(":")[0].strip() comment_map[key] = comment out_lines = iter(data.splitlines()) out_data = "" for line in out_lines: - key = line.split(':')[0].strip() + key = line.split(":")[0].strip() if key in comment_map: out_data += comment_map[key] out_data += "{0}\n".format(line) diff --git a/example/__init__.py b/example/__init__.py index ad44a03..87e1465 100644 --- a/example/__init__.py +++ b/example/__init__.py @@ -1,57 +1,70 @@ """An example application using Confuse for configuration.""" -import confuse + import argparse +import confuse template = { - 'library': confuse.Filename(), - 'import_write': confuse.OneOf([bool, 'ask', 'skip']), - 'ignore': confuse.StrSeq(), - 'plugins': list, - - 'paths': { - 'directory': confuse.Filename(), - 'default': confuse.Filename(relative_to='directory'), + "library": confuse.Filename(), + "import_write": confuse.OneOf([bool, "ask", "skip"]), + "ignore": confuse.StrSeq(), + "plugins": list, + "paths": { + "directory": confuse.Filename(), + "default": confuse.Filename(relative_to="directory"), }, - - 'servers': confuse.Sequence( + "servers": confuse.Sequence( { - 'hostname': str, - 'options': confuse.StrSeq(), + "hostname": str, + "options": confuse.StrSeq(), } - ) + ), } -config = confuse.LazyConfig('ConfuseExample', __name__) +config = confuse.LazyConfig("ConfuseExample", __name__) def main(): - parser = argparse.ArgumentParser(description='example Confuse program') - parser.add_argument('--library', '-l', dest='library', metavar='LIBPATH', - help='library database file') - parser.add_argument('--directory', '-d', dest='paths.directory', - metavar='DIRECTORY', - help='destination music directory') - parser.add_argument('--verbose', '-v', dest='verbose', action='store_true', - help='print debugging messages') + parser = argparse.ArgumentParser(description="example Confuse program") + parser.add_argument( + "--library", + "-l", + dest="library", + metavar="LIBPATH", + help="library database file", + ) + parser.add_argument( + "--directory", + "-d", + dest="paths.directory", + metavar="DIRECTORY", + help="destination music directory", + ) + parser.add_argument( + "--verbose", + "-v", + dest="verbose", + action="store_true", + help="print debugging messages", + ) args = parser.parse_args() config.set_args(args, dots=True) - print('configuration directory is', config.config_dir()) + print("configuration directory is", config.config_dir()) # Use a boolean flag and the transient overlay. - if config['verbose']: - print('verbose mode') - config['log']['level'] = 2 + if config["verbose"]: + print("verbose mode") + config["log"]["level"] = 2 else: - config['log']['level'] = 0 - print('logging level is', config['log']['level'].get(int)) + config["log"]["level"] = 0 + print("logging level is", config["log"]["level"].get(int)) valid = config.get(template) # Some validated/converted values. - print('library is', valid.library) - print('directory is', valid.paths.directory) - print('paths.default is', valid.paths.default) - print('servers are', [s.hostname for s in valid.servers]) + print("library is", valid.library) + print("directory is", valid.paths.directory) + print("paths.default is", valid.paths.default) + print("servers are", [s.hostname for s in valid.servers]) diff --git a/test/__init__.py b/test/__init__.py index e4b6f9d..8f69bb0 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -1,16 +1,17 @@ -import confuse -import tempfile -import shutil import os +import shutil +import tempfile + +import confuse def _root(*sources): return confuse.RootView([confuse.ConfigSource.of(s) for s in sources]) -class TempDir(): - """Context manager that creates and destroys a temporary directory. - """ +class TempDir: + """Context manager that creates and destroys a temporary directory.""" + def __init__(self): self.path = tempfile.mkdtemp() @@ -27,6 +28,6 @@ def sub(self, name, contents=None): """ path = os.path.join(self.path, name) if contents: - with open(path, 'wb') as f: + with open(path, "wb") as f: f.write(contents) return path diff --git a/test/test_cli.py b/test/test_cli.py index 72b861c..76a2879 100644 --- a/test/test_cli.py +++ b/test/test_cli.py @@ -1,13 +1,14 @@ -import confuse import argparse -from argparse import Namespace import optparse import unittest +from argparse import Namespace + +import confuse class ArgparseTest(unittest.TestCase): def setUp(self): - self.config = confuse.Configuration('test', read=False) + self.config = confuse.Configuration("test", read=False) self.parser = argparse.ArgumentParser() def _parse(self, args, **kwargs): @@ -15,70 +16,70 @@ def _parse(self, args, **kwargs): self.config.set_args(args, **kwargs) def test_text_argument_parsed(self): - self.parser.add_argument('--foo', metavar='BAR') - self._parse('--foo bar') - self.assertEqual(self.config['foo'].get(), 'bar') + self.parser.add_argument("--foo", metavar="BAR") + self._parse("--foo bar") + self.assertEqual(self.config["foo"].get(), "bar") def test_boolean_argument_parsed(self): - self.parser.add_argument('--foo', action='store_true') - self._parse('--foo') - self.assertEqual(self.config['foo'].get(), True) + self.parser.add_argument("--foo", action="store_true") + self._parse("--foo") + self.assertEqual(self.config["foo"].get(), True) def test_missing_optional_argument_not_included(self): - self.parser.add_argument('--foo', metavar='BAR') - self._parse('') + self.parser.add_argument("--foo", metavar="BAR") + self._parse("") with self.assertRaises(confuse.NotFoundError): - self.config['foo'].get() + self.config["foo"].get() def test_argument_overrides_default(self): - self.config.add({'foo': 'baz'}) + self.config.add({"foo": "baz"}) - self.parser.add_argument('--foo', metavar='BAR') - self._parse('--foo bar') - self.assertEqual(self.config['foo'].get(), 'bar') + self.parser.add_argument("--foo", metavar="BAR") + self._parse("--foo bar") + self.assertEqual(self.config["foo"].get(), "bar") def test_nested_destination_single(self): - self.parser.add_argument('--one', dest='one.foo') - self.parser.add_argument('--two', dest='one.two.foo') - self._parse('--two TWO', dots=True) - self.assertEqual(self.config['one']['two']['foo'].get(), 'TWO') + self.parser.add_argument("--one", dest="one.foo") + self.parser.add_argument("--two", dest="one.two.foo") + self._parse("--two TWO", dots=True) + self.assertEqual(self.config["one"]["two"]["foo"].get(), "TWO") def test_nested_destination_nested(self): - self.parser.add_argument('--one', dest='one.foo') - self.parser.add_argument('--two', dest='one.two.foo') - self._parse('--two TWO --one ONE', dots=True) - self.assertEqual(self.config['one']['foo'].get(), 'ONE') - self.assertEqual(self.config['one']['two']['foo'].get(), 'TWO') + self.parser.add_argument("--one", dest="one.foo") + self.parser.add_argument("--two", dest="one.two.foo") + self._parse("--two TWO --one ONE", dots=True) + self.assertEqual(self.config["one"]["foo"].get(), "ONE") + self.assertEqual(self.config["one"]["two"]["foo"].get(), "TWO") def test_nested_destination_nested_rev(self): - self.parser.add_argument('--one', dest='one.foo') - self.parser.add_argument('--two', dest='one.two.foo') + self.parser.add_argument("--one", dest="one.foo") + self.parser.add_argument("--two", dest="one.two.foo") # Reverse to ensure order doesn't matter - self._parse('--one ONE --two TWO', dots=True) - self.assertEqual(self.config['one']['foo'].get(), 'ONE') - self.assertEqual(self.config['one']['two']['foo'].get(), 'TWO') + self._parse("--one ONE --two TWO", dots=True) + self.assertEqual(self.config["one"]["foo"].get(), "ONE") + self.assertEqual(self.config["one"]["two"]["foo"].get(), "TWO") def test_nested_destination_clobber(self): - self.parser.add_argument('--one', dest='one.two') - self.parser.add_argument('--two', dest='one.two.foo') - self._parse('--two TWO --one ONE', dots=True) + self.parser.add_argument("--one", dest="one.two") + self.parser.add_argument("--two", dest="one.two.foo") + self._parse("--two TWO --one ONE", dots=True) # Clobbered - self.assertEqual(self.config['one']['two'].get(), {'foo': 'TWO'}) - self.assertEqual(self.config['one']['two']['foo'].get(), 'TWO') + self.assertEqual(self.config["one"]["two"].get(), {"foo": "TWO"}) + self.assertEqual(self.config["one"]["two"]["foo"].get(), "TWO") def test_nested_destination_clobber_rev(self): # Reversed order - self.parser.add_argument('--two', dest='one.two.foo') - self.parser.add_argument('--one', dest='one.two') - self._parse('--one ONE --two TWO', dots=True) + self.parser.add_argument("--two", dest="one.two.foo") + self.parser.add_argument("--one", dest="one.two") + self._parse("--one ONE --two TWO", dots=True) # Clobbered just the same - self.assertEqual(self.config['one']['two'].get(), {'foo': 'TWO'}) - self.assertEqual(self.config['one']['two']['foo'].get(), 'TWO') + self.assertEqual(self.config["one"]["two"].get(), {"foo": "TWO"}) + self.assertEqual(self.config["one"]["two"]["foo"].get(), "TWO") class OptparseTest(unittest.TestCase): def setUp(self): - self.config = confuse.Configuration('test', read=False) + self.config = confuse.Configuration("test", read=False) self.parser = optparse.OptionParser() def _parse(self, args, **kwargs): @@ -86,69 +87,64 @@ def _parse(self, args, **kwargs): self.config.set_args(options, **kwargs) def test_text_argument_parsed(self): - self.parser.add_option('--foo', metavar='BAR') - self._parse('--foo bar') - self.assertEqual(self.config['foo'].get(), 'bar') + self.parser.add_option("--foo", metavar="BAR") + self._parse("--foo bar") + self.assertEqual(self.config["foo"].get(), "bar") def test_boolean_argument_parsed(self): - self.parser.add_option('--foo', action='store_true') - self._parse('--foo') - self.assertEqual(self.config['foo'].get(), True) + self.parser.add_option("--foo", action="store_true") + self._parse("--foo") + self.assertEqual(self.config["foo"].get(), True) def test_missing_optional_argument_not_included(self): - self.parser.add_option('--foo', metavar='BAR') - self._parse('') + self.parser.add_option("--foo", metavar="BAR") + self._parse("") with self.assertRaises(confuse.NotFoundError): - self.config['foo'].get() + self.config["foo"].get() def test_argument_overrides_default(self): - self.config.add({'foo': 'baz'}) + self.config.add({"foo": "baz"}) - self.parser.add_option('--foo', metavar='BAR') - self._parse('--foo bar') - self.assertEqual(self.config['foo'].get(), 'bar') + self.parser.add_option("--foo", metavar="BAR") + self._parse("--foo bar") + self.assertEqual(self.config["foo"].get(), "bar") def test_nested_destination_single(self): - self.parser.add_option('--one', dest='one.foo') - self.parser.add_option('--two', dest='one.two.foo') - self._parse('--two TWO', dots=True) - self.assertEqual(self.config['one']['two']['foo'].get(), 'TWO') + self.parser.add_option("--one", dest="one.foo") + self.parser.add_option("--two", dest="one.two.foo") + self._parse("--two TWO", dots=True) + self.assertEqual(self.config["one"]["two"]["foo"].get(), "TWO") def test_nested_destination_nested(self): - self.parser.add_option('--one', dest='one.foo') - self.parser.add_option('--two', dest='one.two.foo') - self._parse('--two TWO --one ONE', dots=True) - self.assertEqual(self.config['one']['foo'].get(), 'ONE') - self.assertEqual(self.config['one']['two']['foo'].get(), 'TWO') + self.parser.add_option("--one", dest="one.foo") + self.parser.add_option("--two", dest="one.two.foo") + self._parse("--two TWO --one ONE", dots=True) + self.assertEqual(self.config["one"]["foo"].get(), "ONE") + self.assertEqual(self.config["one"]["two"]["foo"].get(), "TWO") def test_nested_destination_nested_rev(self): - self.parser.add_option('--one', dest='one.foo') - self.parser.add_option('--two', dest='one.two.foo') + self.parser.add_option("--one", dest="one.foo") + self.parser.add_option("--two", dest="one.two.foo") # Reverse to ensure order doesn't matter - self._parse('--one ONE --two TWO', dots=True) - self.assertEqual(self.config['one']['foo'].get(), 'ONE') - self.assertEqual(self.config['one']['two']['foo'].get(), 'TWO') + self._parse("--one ONE --two TWO", dots=True) + self.assertEqual(self.config["one"]["foo"].get(), "ONE") + self.assertEqual(self.config["one"]["two"]["foo"].get(), "TWO") class GenericNamespaceTest(unittest.TestCase): def setUp(self): - self.config = confuse.Configuration('test', read=False) + self.config = confuse.Configuration("test", read=False) def test_value_added_to_root(self): - self.config.set_args(Namespace(foo='bar')) - self.assertEqual(self.config['foo'].get(), 'bar') + self.config.set_args(Namespace(foo="bar")) + self.assertEqual(self.config["foo"].get(), "bar") def test_value_added_to_subview(self): - self.config['baz'].set_args(Namespace(foo='bar')) - self.assertEqual(self.config['baz']['foo'].get(), 'bar') + self.config["baz"].set_args(Namespace(foo="bar")) + self.assertEqual(self.config["baz"]["foo"].get(), "bar") def test_nested_namespace(self): - args = Namespace( - first="Hello", - nested=Namespace( - second="World" - ) - ) + args = Namespace(first="Hello", nested=Namespace(second="World")) self.config.set_args(args, dots=True) - self.assertEqual(self.config['first'].get(), 'Hello') - self.assertEqual(self.config['nested']['second'].get(), 'World') + self.assertEqual(self.config["first"].get(), "Hello") + self.assertEqual(self.config["nested"]["second"].get(), "World") diff --git a/test/test_dump.py b/test/test_dump.py index 743b907..75d47e1 100644 --- a/test/test_dump.py +++ b/test/test_dump.py @@ -1,55 +1,60 @@ -import confuse import textwrap import unittest + +import confuse + from . import _root class PrettyDumpTest(unittest.TestCase): def test_dump_null(self): - config = confuse.Configuration('myapp', read=False) - config.add({'foo': None}) + config = confuse.Configuration("myapp", read=False) + config.add({"foo": None}) yaml = config.dump().strip() - self.assertEqual(yaml, 'foo:') + self.assertEqual(yaml, "foo:") def test_dump_true(self): - config = confuse.Configuration('myapp', read=False) - config.add({'foo': True}) + config = confuse.Configuration("myapp", read=False) + config.add({"foo": True}) yaml = config.dump().strip() - self.assertEqual(yaml, 'foo: yes') + self.assertEqual(yaml, "foo: yes") def test_dump_false(self): - config = confuse.Configuration('myapp', read=False) - config.add({'foo': False}) + config = confuse.Configuration("myapp", read=False) + config.add({"foo": False}) yaml = config.dump().strip() - self.assertEqual(yaml, 'foo: no') + self.assertEqual(yaml, "foo: no") def test_dump_short_list(self): - config = confuse.Configuration('myapp', read=False) - config.add({'foo': ['bar', 'baz']}) + config = confuse.Configuration("myapp", read=False) + config.add({"foo": ["bar", "baz"]}) yaml = config.dump().strip() - self.assertEqual(yaml, 'foo: [bar, baz]') + self.assertEqual(yaml, "foo: [bar, baz]") def test_dump_ordered_dict(self): odict = confuse.OrderedDict() - odict['foo'] = 'bar' - odict['bar'] = 'baz' - odict['baz'] = 'qux' + odict["foo"] = "bar" + odict["bar"] = "baz" + odict["baz"] = "qux" - config = confuse.Configuration('myapp', read=False) - config.add({'key': odict}) + config = confuse.Configuration("myapp", read=False) + config.add({"key": odict}) yaml = config.dump().strip() - self.assertEqual(yaml, textwrap.dedent(""" + self.assertEqual( + yaml, + textwrap.dedent(""" key: foo: bar bar: baz baz: qux - """).strip()) + """).strip(), + ) def test_dump_sans_defaults(self): - config = confuse.Configuration('myapp', read=False) - config.add({'foo': 'bar'}) + config = confuse.Configuration("myapp", read=False) + config.add({"foo": "bar"}) config.sources[0].default = True - config.add({'baz': 'qux'}) + config.add({"baz": "qux"}) yaml = config.dump().strip() self.assertEqual(yaml, "foo: bar\nbaz: qux") @@ -60,43 +65,43 @@ def test_dump_sans_defaults(self): class RedactTest(unittest.TestCase): def test_no_redaction(self): - config = _root({'foo': 'bar'}) + config = _root({"foo": "bar"}) data = config.flatten(redact=True) - self.assertEqual(data, {'foo': 'bar'}) + self.assertEqual(data, {"foo": "bar"}) def test_redact_key(self): - config = _root({'foo': 'bar'}) - config['foo'].redact = True + config = _root({"foo": "bar"}) + config["foo"].redact = True data = config.flatten(redact=True) - self.assertEqual(data, {'foo': 'REDACTED'}) + self.assertEqual(data, {"foo": "REDACTED"}) def test_unredact(self): - config = _root({'foo': 'bar'}) - config['foo'].redact = True - config['foo'].redact = False + config = _root({"foo": "bar"}) + config["foo"].redact = True + config["foo"].redact = False data = config.flatten(redact=True) - self.assertEqual(data, {'foo': 'bar'}) + self.assertEqual(data, {"foo": "bar"}) def test_dump_redacted(self): - config = confuse.Configuration('myapp', read=False) - config.add({'foo': 'bar'}) - config['foo'].redact = True + config = confuse.Configuration("myapp", read=False) + config.add({"foo": "bar"}) + config["foo"].redact = True yaml = config.dump(redact=True).strip() - self.assertEqual(yaml, 'foo: REDACTED') + self.assertEqual(yaml, "foo: REDACTED") def test_dump_unredacted(self): - config = confuse.Configuration('myapp', read=False) - config.add({'foo': 'bar'}) - config['foo'].redact = True + config = confuse.Configuration("myapp", read=False) + config.add({"foo": "bar"}) + config["foo"].redact = True yaml = config.dump(redact=False).strip() - self.assertEqual(yaml, 'foo: bar') + self.assertEqual(yaml, "foo: bar") def test_dump_redacted_sans_defaults(self): - config = confuse.Configuration('myapp', read=False) - config.add({'foo': 'bar'}) + config = confuse.Configuration("myapp", read=False) + config.add({"foo": "bar"}) config.sources[0].default = True - config.add({'baz': 'qux'}) - config['baz'].redact = True + config.add({"baz": "qux"}) + config["baz"].redact = True yaml = config.dump(redact=True, full=False).strip() self.assertEqual(yaml, "baz: REDACTED") diff --git a/test/test_env.py b/test/test_env.py index cd8fe85..7fef6d3 100644 --- a/test/test_env.py +++ b/test/test_env.py @@ -1,8 +1,9 @@ -import confuse import os import unittest -from . import _root +import confuse + +from . import _root ENVIRON = os.environ @@ -15,233 +16,233 @@ def tearDown(self): os.environ = ENVIRON def test_prefix(self): - os.environ['TEST_FOO'] = 'a' - os.environ['BAR'] = 'b' - config = _root(confuse.EnvSource('TEST_')) - self.assertEqual(config.get(), {'foo': 'a'}) + os.environ["TEST_FOO"] = "a" + os.environ["BAR"] = "b" + config = _root(confuse.EnvSource("TEST_")) + self.assertEqual(config.get(), {"foo": "a"}) def test_number_type_conversion(self): - os.environ['TEST_FOO'] = '1' - os.environ['TEST_BAR'] = '2.0' - config = _root(confuse.EnvSource('TEST_')) - foo = config['foo'].get() - bar = config['bar'].get() + os.environ["TEST_FOO"] = "1" + os.environ["TEST_BAR"] = "2.0" + config = _root(confuse.EnvSource("TEST_")) + foo = config["foo"].get() + bar = config["bar"].get() self.assertIsInstance(foo, int) self.assertEqual(foo, 1) self.assertIsInstance(bar, float) self.assertEqual(bar, 2.0) def test_bool_type_conversion(self): - os.environ['TEST_FOO'] = 'true' - os.environ['TEST_BAR'] = 'FALSE' - config = _root(confuse.EnvSource('TEST_')) - self.assertIs(config['foo'].get(), True) - self.assertIs(config['bar'].get(), False) + os.environ["TEST_FOO"] = "true" + os.environ["TEST_BAR"] = "FALSE" + config = _root(confuse.EnvSource("TEST_")) + self.assertIs(config["foo"].get(), True) + self.assertIs(config["bar"].get(), False) def test_null_type_conversion(self): - os.environ['TEST_FOO'] = 'null' - os.environ['TEST_BAR'] = '' - config = _root(confuse.EnvSource('TEST_')) - self.assertIs(config['foo'].get(), None) - self.assertIs(config['bar'].get(), None) + os.environ["TEST_FOO"] = "null" + os.environ["TEST_BAR"] = "" + config = _root(confuse.EnvSource("TEST_")) + self.assertIs(config["foo"].get(), None) + self.assertIs(config["bar"].get(), None) def test_unset_lower_config(self): - os.environ['TEST_FOO'] = 'null' - config = _root({'foo': 'bar'}) - self.assertEqual(config['foo'].get(), 'bar') - config.set(confuse.EnvSource('TEST_')) - self.assertIs(config['foo'].get(), None) + os.environ["TEST_FOO"] = "null" + config = _root({"foo": "bar"}) + self.assertEqual(config["foo"].get(), "bar") + config.set(confuse.EnvSource("TEST_")) + self.assertIs(config["foo"].get(), None) def test_sep_default(self): - os.environ['TEST_FOO__BAR'] = 'a' - os.environ['TEST_FOO_BAZ'] = 'b' - config = _root(confuse.EnvSource('TEST_')) - self.assertEqual(config['foo']['bar'].get(), 'a') - self.assertEqual(config['foo_baz'].get(), 'b') + os.environ["TEST_FOO__BAR"] = "a" + os.environ["TEST_FOO_BAZ"] = "b" + config = _root(confuse.EnvSource("TEST_")) + self.assertEqual(config["foo"]["bar"].get(), "a") + self.assertEqual(config["foo_baz"].get(), "b") def test_sep_single_underscore_adjacent_seperators(self): - os.environ['TEST_FOO__BAR'] = 'a' - os.environ['TEST_FOO_BAZ'] = 'b' - config = _root(confuse.EnvSource('TEST_', sep='_')) - self.assertEqual(config['foo']['']['bar'].get(), 'a') - self.assertEqual(config['foo']['baz'].get(), 'b') + os.environ["TEST_FOO__BAR"] = "a" + os.environ["TEST_FOO_BAZ"] = "b" + config = _root(confuse.EnvSource("TEST_", sep="_")) + self.assertEqual(config["foo"][""]["bar"].get(), "a") + self.assertEqual(config["foo"]["baz"].get(), "b") def test_nested(self): - os.environ['TEST_FOO__BAR'] = 'a' - os.environ['TEST_FOO__BAZ__QUX'] = 'b' - config = _root(confuse.EnvSource('TEST_')) - self.assertEqual(config['foo']['bar'].get(), 'a') - self.assertEqual(config['foo']['baz']['qux'].get(), 'b') + os.environ["TEST_FOO__BAR"] = "a" + os.environ["TEST_FOO__BAZ__QUX"] = "b" + config = _root(confuse.EnvSource("TEST_")) + self.assertEqual(config["foo"]["bar"].get(), "a") + self.assertEqual(config["foo"]["baz"]["qux"].get(), "b") def test_nested_rev(self): # Reverse to ensure order doesn't matter - os.environ['TEST_FOO__BAZ__QUX'] = 'b' - os.environ['TEST_FOO__BAR'] = 'a' - config = _root(confuse.EnvSource('TEST_')) - self.assertEqual(config['foo']['bar'].get(), 'a') - self.assertEqual(config['foo']['baz']['qux'].get(), 'b') + os.environ["TEST_FOO__BAZ__QUX"] = "b" + os.environ["TEST_FOO__BAR"] = "a" + config = _root(confuse.EnvSource("TEST_")) + self.assertEqual(config["foo"]["bar"].get(), "a") + self.assertEqual(config["foo"]["baz"]["qux"].get(), "b") def test_nested_clobber(self): - os.environ['TEST_FOO__BAR'] = 'a' - os.environ['TEST_FOO__BAR__BAZ'] = 'b' - config = _root(confuse.EnvSource('TEST_')) + os.environ["TEST_FOO__BAR"] = "a" + os.environ["TEST_FOO__BAR__BAZ"] = "b" + config = _root(confuse.EnvSource("TEST_")) # Clobbered - self.assertEqual(config['foo']['bar'].get(), {'baz': 'b'}) - self.assertEqual(config['foo']['bar']['baz'].get(), 'b') + self.assertEqual(config["foo"]["bar"].get(), {"baz": "b"}) + self.assertEqual(config["foo"]["bar"]["baz"].get(), "b") def test_nested_clobber_rev(self): # Reverse to ensure order doesn't matter - os.environ['TEST_FOO__BAR__BAZ'] = 'b' - os.environ['TEST_FOO__BAR'] = 'a' - config = _root(confuse.EnvSource('TEST_')) + os.environ["TEST_FOO__BAR__BAZ"] = "b" + os.environ["TEST_FOO__BAR"] = "a" + config = _root(confuse.EnvSource("TEST_")) # Clobbered - self.assertEqual(config['foo']['bar'].get(), {'baz': 'b'}) - self.assertEqual(config['foo']['bar']['baz'].get(), 'b') + self.assertEqual(config["foo"]["bar"].get(), {"baz": "b"}) + self.assertEqual(config["foo"]["bar"]["baz"].get(), "b") def test_lower_applied_after_prefix_match(self): - os.environ['TEST_FOO'] = 'a' - config = _root(confuse.EnvSource('test_', lower=True)) + os.environ["TEST_FOO"] = "a" + config = _root(confuse.EnvSource("test_", lower=True)) self.assertEqual(config.get(), {}) def test_lower_already_lowercase(self): - os.environ['TEST_foo'] = 'a' - config = _root(confuse.EnvSource('TEST_', lower=True)) - self.assertEqual(config.get(), {'foo': 'a'}) + os.environ["TEST_foo"] = "a" + config = _root(confuse.EnvSource("TEST_", lower=True)) + self.assertEqual(config.get(), {"foo": "a"}) def test_lower_does_not_alter_value(self): - os.environ['TEST_FOO'] = 'UPPER' - config = _root(confuse.EnvSource('TEST_', lower=True)) - self.assertEqual(config.get(), {'foo': 'UPPER'}) + os.environ["TEST_FOO"] = "UPPER" + config = _root(confuse.EnvSource("TEST_", lower=True)) + self.assertEqual(config.get(), {"foo": "UPPER"}) def test_lower_false(self): - os.environ['TEST_FOO'] = 'a' - config = _root(confuse.EnvSource('TEST_', lower=False)) - self.assertEqual(config.get(), {'FOO': 'a'}) + os.environ["TEST_FOO"] = "a" + config = _root(confuse.EnvSource("TEST_", lower=False)) + self.assertEqual(config.get(), {"FOO": "a"}) def test_handle_lists_good_list(self): - os.environ['TEST_FOO__0'] = 'a' - os.environ['TEST_FOO__1'] = 'b' - os.environ['TEST_FOO__2'] = 'c' - config = _root(confuse.EnvSource('TEST_', handle_lists=True)) - self.assertEqual(config['foo'].get(), ['a', 'b', 'c']) + os.environ["TEST_FOO__0"] = "a" + os.environ["TEST_FOO__1"] = "b" + os.environ["TEST_FOO__2"] = "c" + config = _root(confuse.EnvSource("TEST_", handle_lists=True)) + self.assertEqual(config["foo"].get(), ["a", "b", "c"]) def test_handle_lists_good_list_rev(self): # Reverse to ensure order doesn't matter - os.environ['TEST_FOO__2'] = 'c' - os.environ['TEST_FOO__1'] = 'b' - os.environ['TEST_FOO__0'] = 'a' - config = _root(confuse.EnvSource('TEST_', handle_lists=True)) - self.assertEqual(config['foo'].get(), ['a', 'b', 'c']) + os.environ["TEST_FOO__2"] = "c" + os.environ["TEST_FOO__1"] = "b" + os.environ["TEST_FOO__0"] = "a" + config = _root(confuse.EnvSource("TEST_", handle_lists=True)) + self.assertEqual(config["foo"].get(), ["a", "b", "c"]) def test_handle_lists_nested_lists(self): - os.environ['TEST_FOO__0__0'] = 'a' - os.environ['TEST_FOO__0__1'] = 'b' - os.environ['TEST_FOO__1__0'] = 'c' - config = _root(confuse.EnvSource('TEST_', handle_lists=True)) - self.assertEqual(config['foo'].get(), [['a', 'b'], ['c']]) + os.environ["TEST_FOO__0__0"] = "a" + os.environ["TEST_FOO__0__1"] = "b" + os.environ["TEST_FOO__1__0"] = "c" + config = _root(confuse.EnvSource("TEST_", handle_lists=True)) + self.assertEqual(config["foo"].get(), [["a", "b"], ["c"]]) def test_handle_lists_bad_list_missing_index(self): - os.environ['TEST_FOO__0'] = 'a' - os.environ['TEST_FOO__2'] = 'b' - os.environ['TEST_FOO__3'] = 'c' - config = _root(confuse.EnvSource('TEST_', handle_lists=True)) - self.assertEqual(config['foo'].get(), {'0': 'a', '2': 'b', '3': 'c'}) + os.environ["TEST_FOO__0"] = "a" + os.environ["TEST_FOO__2"] = "b" + os.environ["TEST_FOO__3"] = "c" + config = _root(confuse.EnvSource("TEST_", handle_lists=True)) + self.assertEqual(config["foo"].get(), {"0": "a", "2": "b", "3": "c"}) def test_handle_lists_bad_list_non_zero_start(self): - os.environ['TEST_FOO__1'] = 'a' - os.environ['TEST_FOO__2'] = 'b' - os.environ['TEST_FOO__3'] = 'c' - config = _root(confuse.EnvSource('TEST_', handle_lists=True)) - self.assertEqual(config['foo'].get(), {'1': 'a', '2': 'b', '3': 'c'}) + os.environ["TEST_FOO__1"] = "a" + os.environ["TEST_FOO__2"] = "b" + os.environ["TEST_FOO__3"] = "c" + config = _root(confuse.EnvSource("TEST_", handle_lists=True)) + self.assertEqual(config["foo"].get(), {"1": "a", "2": "b", "3": "c"}) def test_handle_lists_bad_list_non_numeric(self): - os.environ['TEST_FOO__0'] = 'a' - os.environ['TEST_FOO__ONE'] = 'b' - os.environ['TEST_FOO__2'] = 'c' - config = _root(confuse.EnvSource('TEST_', handle_lists=True)) - self.assertEqual(config['foo'].get(), {'0': 'a', 'one': 'b', '2': 'c'}) + os.environ["TEST_FOO__0"] = "a" + os.environ["TEST_FOO__ONE"] = "b" + os.environ["TEST_FOO__2"] = "c" + config = _root(confuse.EnvSource("TEST_", handle_lists=True)) + self.assertEqual(config["foo"].get(), {"0": "a", "one": "b", "2": "c"}) def test_handle_lists_top_level_always_dict(self): - os.environ['TEST_0'] = 'a' - os.environ['TEST_1'] = 'b' - os.environ['TEST_2'] = 'c' - config = _root(confuse.EnvSource('TEST_', handle_lists=True)) - self.assertEqual(config.get(), {'0': 'a', '1': 'b', '2': 'c'}) + os.environ["TEST_0"] = "a" + os.environ["TEST_1"] = "b" + os.environ["TEST_2"] = "c" + config = _root(confuse.EnvSource("TEST_", handle_lists=True)) + self.assertEqual(config.get(), {"0": "a", "1": "b", "2": "c"}) def test_handle_lists_not_a_list(self): - os.environ['TEST_FOO__BAR'] = 'a' - os.environ['TEST_FOO__BAZ'] = 'b' - config = _root(confuse.EnvSource('TEST_', handle_lists=True)) - self.assertEqual(config['foo'].get(), {'bar': 'a', 'baz': 'b'}) + os.environ["TEST_FOO__BAR"] = "a" + os.environ["TEST_FOO__BAZ"] = "b" + config = _root(confuse.EnvSource("TEST_", handle_lists=True)) + self.assertEqual(config["foo"].get(), {"bar": "a", "baz": "b"}) def test_parse_yaml_docs_scalar(self): - os.environ['TEST_FOO'] = 'a' - config = _root(confuse.EnvSource('TEST_', parse_yaml_docs=True)) - self.assertEqual(config['foo'].get(), 'a') + os.environ["TEST_FOO"] = "a" + config = _root(confuse.EnvSource("TEST_", parse_yaml_docs=True)) + self.assertEqual(config["foo"].get(), "a") def test_parse_yaml_docs_list(self): - os.environ['TEST_FOO'] = '[a, b]' - config = _root(confuse.EnvSource('TEST_', parse_yaml_docs=True)) - self.assertEqual(config['foo'].get(), ['a', 'b']) + os.environ["TEST_FOO"] = "[a, b]" + config = _root(confuse.EnvSource("TEST_", parse_yaml_docs=True)) + self.assertEqual(config["foo"].get(), ["a", "b"]) def test_parse_yaml_docs_dict(self): - os.environ['TEST_FOO'] = '{bar: a, baz: b}' - config = _root(confuse.EnvSource('TEST_', parse_yaml_docs=True)) - self.assertEqual(config['foo'].get(), {'bar': 'a', 'baz': 'b'}) + os.environ["TEST_FOO"] = "{bar: a, baz: b}" + config = _root(confuse.EnvSource("TEST_", parse_yaml_docs=True)) + self.assertEqual(config["foo"].get(), {"bar": "a", "baz": "b"}) def test_parse_yaml_docs_nested(self): - os.environ['TEST_FOO'] = '{bar: [a, b], baz: {qux: c}}' - config = _root(confuse.EnvSource('TEST_', parse_yaml_docs=True)) - self.assertEqual(config['foo']['bar'].get(), ['a', 'b']) - self.assertEqual(config['foo']['baz'].get(), {'qux': 'c'}) + os.environ["TEST_FOO"] = "{bar: [a, b], baz: {qux: c}}" + config = _root(confuse.EnvSource("TEST_", parse_yaml_docs=True)) + self.assertEqual(config["foo"]["bar"].get(), ["a", "b"]) + self.assertEqual(config["foo"]["baz"].get(), {"qux": "c"}) def test_parse_yaml_docs_number_conversion(self): - os.environ['TEST_FOO'] = '{bar: 1, baz: 2.0}' - config = _root(confuse.EnvSource('TEST_', parse_yaml_docs=True)) - bar = config['foo']['bar'].get() - baz = config['foo']['baz'].get() + os.environ["TEST_FOO"] = "{bar: 1, baz: 2.0}" + config = _root(confuse.EnvSource("TEST_", parse_yaml_docs=True)) + bar = config["foo"]["bar"].get() + baz = config["foo"]["baz"].get() self.assertIsInstance(bar, int) self.assertEqual(bar, 1) self.assertIsInstance(baz, float) self.assertEqual(baz, 2.0) def test_parse_yaml_docs_bool_conversion(self): - os.environ['TEST_FOO'] = '{bar: true, baz: FALSE}' - config = _root(confuse.EnvSource('TEST_', parse_yaml_docs=True)) - self.assertIs(config['foo']['bar'].get(), True) - self.assertIs(config['foo']['baz'].get(), False) + os.environ["TEST_FOO"] = "{bar: true, baz: FALSE}" + config = _root(confuse.EnvSource("TEST_", parse_yaml_docs=True)) + self.assertIs(config["foo"]["bar"].get(), True) + self.assertIs(config["foo"]["baz"].get(), False) def test_parse_yaml_docs_null_conversion(self): - os.environ['TEST_FOO'] = '{bar: null, baz: }' - config = _root(confuse.EnvSource('TEST_', parse_yaml_docs=True)) - self.assertIs(config['foo']['bar'].get(), None) - self.assertIs(config['foo']['baz'].get(), None) + os.environ["TEST_FOO"] = "{bar: null, baz: }" + config = _root(confuse.EnvSource("TEST_", parse_yaml_docs=True)) + self.assertIs(config["foo"]["bar"].get(), None) + self.assertIs(config["foo"]["baz"].get(), None) def test_parse_yaml_docs_syntax_error(self): - os.environ['TEST_FOO'] = '{:}' + os.environ["TEST_FOO"] = "{:}" try: - _root(confuse.EnvSource('TEST_', parse_yaml_docs=True)) + _root(confuse.EnvSource("TEST_", parse_yaml_docs=True)) except confuse.ConfigError as exc: - self.assertTrue('TEST_FOO' in exc.name) + self.assertTrue("TEST_FOO" in exc.name) else: - self.fail('ConfigError not raised') + self.fail("ConfigError not raised") def test_parse_yaml_docs_false(self): - os.environ['TEST_FOO'] = '{bar: a, baz: b}' - config = _root(confuse.EnvSource('TEST_', parse_yaml_docs=False)) - self.assertEqual(config['foo'].get(), '{bar: a, baz: b}') + os.environ["TEST_FOO"] = "{bar: a, baz: b}" + config = _root(confuse.EnvSource("TEST_", parse_yaml_docs=False)) + self.assertEqual(config["foo"].get(), "{bar: a, baz: b}") with self.assertRaises(confuse.ConfigError): - config['foo']['bar'].get() + config["foo"]["bar"].get() class ConfigEnvTest(unittest.TestCase): def setUp(self): - self.config = confuse.Configuration('TestApp', read=False) + self.config = confuse.Configuration("TestApp", read=False) os.environ = { - 'TESTAPP_FOO': 'a', - 'TESTAPP_BAR__NESTED': 'b', - 'TESTAPP_BAZ_SEP_NESTED': 'c', - 'MYAPP_QUX_SEP_NESTED': 'd' + "TESTAPP_FOO": "a", + "TESTAPP_BAR__NESTED": "b", + "TESTAPP_BAZ_SEP_NESTED": "c", + "MYAPP_QUX_SEP_NESTED": "d", } def tearDown(self): @@ -249,20 +250,21 @@ def tearDown(self): def test_defaults(self): self.config.set_env() - self.assertEqual(self.config.get(), {'foo': 'a', - 'bar': {'nested': 'b'}, - 'baz_sep_nested': 'c'}) + self.assertEqual( + self.config.get(), + {"foo": "a", "bar": {"nested": "b"}, "baz_sep_nested": "c"}, + ) def test_with_prefix(self): - self.config.set_env(prefix='MYAPP_') - self.assertEqual(self.config.get(), {'qux_sep_nested': 'd'}) + self.config.set_env(prefix="MYAPP_") + self.assertEqual(self.config.get(), {"qux_sep_nested": "d"}) def test_with_sep(self): - self.config.set_env(sep='_sep_') - self.assertEqual(self.config.get(), {'foo': 'a', - 'bar__nested': 'b', - 'baz': {'nested': 'c'}}) + self.config.set_env(sep="_sep_") + self.assertEqual( + self.config.get(), {"foo": "a", "bar__nested": "b", "baz": {"nested": "c"}} + ) def test_with_prefix_and_sep(self): - self.config.set_env(prefix='MYAPP_', sep='_sep_') - self.assertEqual(self.config.get(), {'qux': {'nested': 'd'}}) + self.config.set_env(prefix="MYAPP_", sep="_sep_") + self.assertEqual(self.config.get(), {"qux": {"nested": "d"}}) diff --git a/test/test_paths.py b/test/test_paths.py index c832b5e..9d1d30b 100644 --- a/test/test_paths.py +++ b/test/test_paths.py @@ -1,5 +1,3 @@ -import confuse -import confuse.yaml_util import ntpath import os import platform @@ -8,23 +6,27 @@ import tempfile import unittest +import confuse +import confuse.yaml_util DEFAULT = [platform.system, os.environ, os.path] SYSTEMS = { - 'Linux': [{'HOME': '/home/test', 'XDG_CONFIG_HOME': '~/xdgconfig'}, - posixpath], - 'Darwin': [{'HOME': '/Users/test'}, posixpath], - 'Windows': [{ - 'APPDATA': '~\\winconfig', - 'HOME': 'C:\\Users\\test', - 'USERPROFILE': 'C:\\Users\\test', - }, ntpath] + "Linux": [{"HOME": "/home/test", "XDG_CONFIG_HOME": "~/xdgconfig"}, posixpath], + "Darwin": [{"HOME": "/Users/test"}, posixpath], + "Windows": [ + { + "APPDATA": "~\\winconfig", + "HOME": "C:\\Users\\test", + "USERPROFILE": "C:\\Users\\test", + }, + ntpath, + ], } def _touch(path): - open(path, 'a').close() + open(path, "a").close() class FakeSystem(unittest.TestCase): @@ -44,71 +46,90 @@ def setUp(self): platform.system = lambda: self.SYS_NAME if self.TMP_HOME: - os.environ['HOME'] = self.home - os.environ['USERPROFILE'] = self.home + os.environ["HOME"] = self.home + os.environ["USERPROFILE"] = self.home def tearDown(self): platform.system, os.environ, os.path = DEFAULT - if hasattr(self, 'home'): + if hasattr(self, "home"): shutil.rmtree(self.home) class LinuxTestCases(FakeSystem): - SYS_NAME = 'Linux' + SYS_NAME = "Linux" def test_both_xdg_and_fallback_dirs(self): - self.assertEqual(confuse.config_dirs(), - ['/home/test/.config', '/home/test/xdgconfig', - '/etc/xdg', '/etc']) + self.assertEqual( + confuse.config_dirs(), + ["/home/test/.config", "/home/test/xdgconfig", "/etc/xdg", "/etc"], + ) def test_fallback_only(self): - del os.environ['XDG_CONFIG_HOME'] - self.assertEqual(confuse.config_dirs(), ['/home/test/.config', - '/etc/xdg', '/etc']) + del os.environ["XDG_CONFIG_HOME"] + self.assertEqual( + confuse.config_dirs(), ["/home/test/.config", "/etc/xdg", "/etc"] + ) def test_xdg_matching_fallback_not_duplicated(self): - os.environ['XDG_CONFIG_HOME'] = '~/.config' - self.assertEqual(confuse.config_dirs(), ['/home/test/.config', - '/etc/xdg', '/etc']) + os.environ["XDG_CONFIG_HOME"] = "~/.config" + self.assertEqual( + confuse.config_dirs(), ["/home/test/.config", "/etc/xdg", "/etc"] + ) def test_xdg_config_dirs(self): - os.environ['XDG_CONFIG_DIRS'] = '/usr/local/etc/xdg:/etc/xdg' - self.assertEqual(confuse.config_dirs(), ['/home/test/.config', - '/home/test/xdgconfig', - '/usr/local/etc/xdg', - '/etc/xdg', '/etc']) + os.environ["XDG_CONFIG_DIRS"] = "/usr/local/etc/xdg:/etc/xdg" + self.assertEqual( + confuse.config_dirs(), + [ + "/home/test/.config", + "/home/test/xdgconfig", + "/usr/local/etc/xdg", + "/etc/xdg", + "/etc", + ], + ) class OSXTestCases(FakeSystem): - SYS_NAME = 'Darwin' + SYS_NAME = "Darwin" def test_mac_dirs(self): - self.assertEqual(confuse.config_dirs(), - ['/Users/test/.config', - '/Users/test/Library/Application Support', - '/etc/xdg', '/etc']) + self.assertEqual( + confuse.config_dirs(), + [ + "/Users/test/.config", + "/Users/test/Library/Application Support", + "/etc/xdg", + "/etc", + ], + ) def test_xdg_config_dirs(self): - os.environ['XDG_CONFIG_DIRS'] = '/usr/local/etc/xdg:/etc/xdg' - self.assertEqual(confuse.config_dirs(), - ['/Users/test/.config', - '/Users/test/Library/Application Support', - '/usr/local/etc/xdg', - '/etc/xdg', '/etc']) + os.environ["XDG_CONFIG_DIRS"] = "/usr/local/etc/xdg:/etc/xdg" + self.assertEqual( + confuse.config_dirs(), + [ + "/Users/test/.config", + "/Users/test/Library/Application Support", + "/usr/local/etc/xdg", + "/etc/xdg", + "/etc", + ], + ) class WindowsTestCases(FakeSystem): - SYS_NAME = 'Windows' + SYS_NAME = "Windows" def test_dir_from_environ(self): - self.assertEqual(confuse.config_dirs(), - ['C:\\Users\\test\\AppData\\Roaming', - 'C:\\Users\\test\\winconfig']) + self.assertEqual( + confuse.config_dirs(), + ["C:\\Users\\test\\AppData\\Roaming", "C:\\Users\\test\\winconfig"], + ) def test_fallback_dir(self): - del os.environ['APPDATA'] - self.assertEqual(confuse.config_dirs(), - ['C:\\Users\\test\\AppData\\Roaming']) + del os.environ["APPDATA"] + self.assertEqual(confuse.config_dirs(), ["C:\\Users\\test\\AppData\\Roaming"]) class ConfigFilenamesTest(unittest.TestCase): @@ -121,12 +142,12 @@ def tearDown(self): confuse.yaml_util.load_yaml, os.path.isfile = self._old def test_no_sources_when_files_missing(self): - config = confuse.Configuration('myapp', read=False) + config = confuse.Configuration("myapp", read=False) filenames = [s.filename for s in config.sources] self.assertEqual(filenames, []) def test_search_package(self): - config = confuse.Configuration('myapp', __name__, read=False) + config = confuse.Configuration("myapp", __name__, read=False) config._add_default_source() for source in config.sources: @@ -138,7 +159,7 @@ def test_search_package(self): self.assertEqual( default_source.filename, - os.path.join(os.path.dirname(__file__), 'config_default.yaml') + os.path.join(os.path.dirname(__file__), "config_default.yaml"), ) self.assertTrue(source.default) @@ -148,25 +169,25 @@ class EnvVarTest(FakeSystem): def setUp(self): super(EnvVarTest, self).setUp() - self.config = confuse.Configuration('myapp', read=False) - os.environ['MYAPPDIR'] = self.home # use the tmp home as a config dir + self.config = confuse.Configuration("myapp", read=False) + os.environ["MYAPPDIR"] = self.home # use the tmp home as a config dir def test_env_var_name(self): - self.assertEqual(self.config._env_var, 'MYAPPDIR') + self.assertEqual(self.config._env_var, "MYAPPDIR") def test_env_var_dir_has_first_priority(self): self.assertEqual(self.config.config_dir(), self.home) def test_env_var_missing(self): - del os.environ['MYAPPDIR'] + del os.environ["MYAPPDIR"] self.assertNotEqual(self.config.config_dir(), self.home) class PrimaryConfigDirTest(FakeSystem): - SYS_NAME = 'Linux' # conversion from posix to nt is easy + SYS_NAME = "Linux" # conversion from posix to nt is easy TMP_HOME = True - if platform.system() == 'Windows': + if platform.system() == "Windows": # wrap these functions as they need to work on the host system which is # only needed on Windows as we are using `posixpath` def join(self, *args): @@ -179,33 +200,33 @@ def makedirs(self, path, *args, **kwargs): def setUp(self): super(PrimaryConfigDirTest, self).setUp() - if hasattr(self, 'join'): + if hasattr(self, "join"): os.path.join = self.join os.makedirs, self._makedirs = self.makedirs, os.makedirs - self.config = confuse.Configuration('test', read=False) + self.config = confuse.Configuration("test", read=False) def tearDown(self): super(PrimaryConfigDirTest, self).tearDown() - if hasattr(self, '_makedirs'): + if hasattr(self, "_makedirs"): os.makedirs = self._makedirs def test_create_dir_if_none_exists(self): - path = os.path.join(self.home, '.config', 'test') + path = os.path.join(self.home, ".config", "test") assert not os.path.exists(path) self.assertEqual(self.config.config_dir(), path) self.assertTrue(os.path.isdir(path)) def test_return_existing_dir(self): - path = os.path.join(self.home, 'xdgconfig', 'test') + path = os.path.join(self.home, "xdgconfig", "test") os.makedirs(path) _touch(os.path.join(path, confuse.CONFIG_FILENAME)) self.assertEqual(self.config.config_dir(), path) def test_do_not_create_dir_if_lower_priority_exists(self): - path1 = os.path.join(self.home, 'xdgconfig', 'test') - path2 = os.path.join(self.home, '.config', 'test') + path1 = os.path.join(self.home, "xdgconfig", "test") + path2 = os.path.join(self.home, ".config", "test") os.makedirs(path2) _touch(os.path.join(path2, confuse.CONFIG_FILENAME)) assert not os.path.exists(path1) diff --git a/test/test_utils.py b/test/test_utils.py index b8ebc9c..632b3e3 100644 --- a/test/test_utils.py +++ b/test/test_utils.py @@ -1,75 +1,76 @@ +import unittest from argparse import Namespace from collections import OrderedDict + import confuse -import unittest class BuildDictTests(unittest.TestCase): def test_pure_dicts(self): - config = {'foo': {'bar': 1}} + config = {"foo": {"bar": 1}} result = confuse.util.build_dict(config) - self.assertEqual(1, result['foo']['bar']) + self.assertEqual(1, result["foo"]["bar"]) def test_namespaces(self): config = Namespace(foo=Namespace(bar=2), another=1) result = confuse.util.build_dict(config) - self.assertEqual(2, result['foo']['bar']) - self.assertEqual(1, result['another']) + self.assertEqual(2, result["foo"]["bar"]) + self.assertEqual(1, result["another"]) def test_dot_sep_keys(self): - config = {'foo.bar': 1} + config = {"foo.bar": 1} result = confuse.util.build_dict(config.copy()) - self.assertEqual(1, result['foo.bar']) + self.assertEqual(1, result["foo.bar"]) - result = confuse.util.build_dict(config.copy(), sep='.') - self.assertEqual(1, result['foo']['bar']) + result = confuse.util.build_dict(config.copy(), sep=".") + self.assertEqual(1, result["foo"]["bar"]) def test_dot_sep_keys_clobber(self): - args = [('foo.bar', 1), ('foo.bar.zar', 2)] + args = [("foo.bar", 1), ("foo.bar.zar", 2)] config = OrderedDict(args) - result = confuse.util.build_dict(config.copy(), sep='.') - self.assertEqual({'zar': 2}, result['foo']['bar']) - self.assertEqual(2, result['foo']['bar']['zar']) + result = confuse.util.build_dict(config.copy(), sep=".") + self.assertEqual({"zar": 2}, result["foo"]["bar"]) + self.assertEqual(2, result["foo"]["bar"]["zar"]) # Reverse and do it again! (should be stable) args.reverse() config = OrderedDict(args) - result = confuse.util.build_dict(config.copy(), sep='.') - self.assertEqual({'zar': 2}, result['foo']['bar']) - self.assertEqual(2, result['foo']['bar']['zar']) + result = confuse.util.build_dict(config.copy(), sep=".") + self.assertEqual({"zar": 2}, result["foo"]["bar"]) + self.assertEqual(2, result["foo"]["bar"]["zar"]) def test_dot_sep_keys_no_clobber(self): - args = [('foo.bar', 1), ('foo.far', 2), ('foo.zar.dar', 4)] + args = [("foo.bar", 1), ("foo.far", 2), ("foo.zar.dar", 4)] config = OrderedDict(args) - result = confuse.util.build_dict(config.copy(), sep='.') - self.assertEqual(1, result['foo']['bar']) - self.assertEqual(2, result['foo']['far']) - self.assertEqual(4, result['foo']['zar']['dar']) + result = confuse.util.build_dict(config.copy(), sep=".") + self.assertEqual(1, result["foo"]["bar"]) + self.assertEqual(2, result["foo"]["far"]) + self.assertEqual(4, result["foo"]["zar"]["dar"]) def test_adjacent_underscores_sep_keys(self): - config = {'foo__bar_baz': 1} + config = {"foo__bar_baz": 1} result = confuse.util.build_dict(config.copy()) - self.assertEqual(1, result['foo__bar_baz']) + self.assertEqual(1, result["foo__bar_baz"]) - result = confuse.util.build_dict(config.copy(), sep='_') - self.assertEqual(1, result['foo']['']['bar']['baz']) + result = confuse.util.build_dict(config.copy(), sep="_") + self.assertEqual(1, result["foo"][""]["bar"]["baz"]) - result = confuse.util.build_dict(config.copy(), sep='__') - self.assertEqual(1, result['foo']['bar_baz']) + result = confuse.util.build_dict(config.copy(), sep="__") + self.assertEqual(1, result["foo"]["bar_baz"]) def test_keep_none(self): - config = {'foo': None} + config = {"foo": None} result = confuse.util.build_dict(config.copy()) with self.assertRaises(KeyError): - result['foo'] + result["foo"] result = confuse.util.build_dict(config.copy(), keep_none=True) - self.assertIs(None, result['foo']) + self.assertIs(None, result["foo"]) def test_keep_none_with_nested(self): - config = {'foo': {'bar': None}} + config = {"foo": {"bar": None}} result = confuse.util.build_dict(config.copy()) - self.assertEqual({}, result['foo']) + self.assertEqual({}, result["foo"]) result = confuse.util.build_dict(config.copy(), keep_none=True) - self.assertIs(None, result['foo']['bar']) + self.assertIs(None, result["foo"]["bar"]) diff --git a/test/test_valid.py b/test/test_valid.py index 701cd68..623712a 100644 --- a/test/test_valid.py +++ b/test/test_valid.py @@ -1,85 +1,95 @@ -from collections.abc import Mapping, Sequence -import confuse import enum import os import unittest +from collections.abc import Mapping, Sequence + +import confuse + from . import _root class ValidConfigTest(unittest.TestCase): def test_validate_simple_dict(self): - config = _root({'foo': 5}) - valid = config.get({'foo': confuse.Integer()}) - self.assertEqual(valid['foo'], 5) + config = _root({"foo": 5}) + valid = config.get({"foo": confuse.Integer()}) + self.assertEqual(valid["foo"], 5) def test_default_value(self): config = _root({}) - valid = config.get({'foo': confuse.Integer(8)}) - self.assertEqual(valid['foo'], 8) + valid = config.get({"foo": confuse.Integer(8)}) + self.assertEqual(valid["foo"], 8) def test_undeclared_key_raises_keyerror(self): - config = _root({'foo': 5}) - valid = config.get({'foo': confuse.Integer()}) + config = _root({"foo": 5}) + valid = config.get({"foo": confuse.Integer()}) with self.assertRaises(KeyError): - valid['bar'] + valid["bar"] def test_undeclared_key_ignored_from_input(self): - config = _root({'foo': 5, 'bar': 6}) - valid = config.get({'foo': confuse.Integer()}) + config = _root({"foo": 5, "bar": 6}) + valid = config.get({"foo": confuse.Integer()}) with self.assertRaises(KeyError): - valid['bar'] + valid["bar"] def test_int_template_shortcut(self): - config = _root({'foo': 5}) - valid = config.get({'foo': int}) - self.assertEqual(valid['foo'], 5) + config = _root({"foo": 5}) + valid = config.get({"foo": int}) + self.assertEqual(valid["foo"], 5) def test_int_default_shortcut(self): config = _root({}) - valid = config.get({'foo': 9}) - self.assertEqual(valid['foo'], 9) + valid = config.get({"foo": 9}) + self.assertEqual(valid["foo"], 9) def test_attribute_access(self): - config = _root({'foo': 5}) - valid = config.get({'foo': confuse.Integer()}) + config = _root({"foo": 5}) + valid = config.get({"foo": confuse.Integer()}) self.assertEqual(valid.foo, 5) def test_missing_required_value_raises_error_on_validate(self): config = _root({}) with self.assertRaises(confuse.NotFoundError): - config.get({'foo': confuse.Integer()}) + config.get({"foo": confuse.Integer()}) def test_none_as_default(self): config = _root({}) - valid = config.get({'foo': confuse.Integer(None)}) - self.assertIsNone(valid['foo']) + valid = config.get({"foo": confuse.Integer(None)}) + self.assertIsNone(valid["foo"]) def test_wrong_type_raises_error_on_validate(self): - config = _root({'foo': 'bar'}) + config = _root({"foo": "bar"}) with self.assertRaises(confuse.ConfigTypeError): - config.get({'foo': confuse.Integer()}) + config.get({"foo": confuse.Integer()}) def test_validate_individual_value(self): - config = _root({'foo': 5}) - valid = config['foo'].get(confuse.Integer()) + config = _root({"foo": 5}) + valid = config["foo"].get(confuse.Integer()) self.assertEqual(valid, 5) def test_nested_dict_template(self): - config = _root({ - 'foo': {'bar': 9}, - }) - valid = config.get({ - 'foo': {'bar': confuse.Integer()}, - }) - self.assertEqual(valid['foo']['bar'], 9) + config = _root( + { + "foo": {"bar": 9}, + } + ) + valid = config.get( + { + "foo": {"bar": confuse.Integer()}, + } + ) + self.assertEqual(valid["foo"]["bar"], 9) def test_nested_attribute_access(self): - config = _root({ - 'foo': {'bar': 8}, - }) - valid = config.get({ - 'foo': {'bar': confuse.Integer()}, - }) + config = _root( + { + "foo": {"bar": 8}, + } + ) + valid = config.get( + { + "foo": {"bar": confuse.Integer()}, + } + ) self.assertEqual(valid.foo.bar, 8) @@ -100,25 +110,24 @@ def test_plain_string_as_template(self): self.assertEqual(typ.default, confuse.REQUIRED) def test_concrete_string_as_template(self): - typ = confuse.as_template('foo') + typ = confuse.as_template("foo") self.assertIsInstance(typ, confuse.String) - self.assertEqual(typ.default, 'foo') + self.assertEqual(typ.default, "foo") def test_dict_as_template(self): - typ = confuse.as_template({'key': 9}) + typ = confuse.as_template({"key": 9}) self.assertIsInstance(typ, confuse.MappingTemplate) - self.assertIsInstance(typ.subtemplates['key'], confuse.Integer) - self.assertEqual(typ.subtemplates['key'].default, 9) + self.assertIsInstance(typ.subtemplates["key"], confuse.Integer) + self.assertEqual(typ.subtemplates["key"].default, 9) def test_nested_dict_as_template(self): - typ = confuse.as_template({'outer': {'inner': 2}}) + typ = confuse.as_template({"outer": {"inner": 2}}) self.assertIsInstance(typ, confuse.MappingTemplate) - self.assertIsInstance(typ.subtemplates['outer'], - confuse.MappingTemplate) - self.assertIsInstance(typ.subtemplates['outer'].subtemplates['inner'], - confuse.Integer) - self.assertEqual(typ.subtemplates['outer'].subtemplates['inner'] - .default, 2) + self.assertIsInstance(typ.subtemplates["outer"], confuse.MappingTemplate) + self.assertIsInstance( + typ.subtemplates["outer"].subtemplates["inner"], confuse.Integer + ) + self.assertEqual(typ.subtemplates["outer"].subtemplates["inner"].default, 2) def test_list_as_template(self): typ = confuse.as_template(list()) @@ -139,9 +148,9 @@ def test_float_type_as_tempalte(self): self.assertEqual(typ.default, confuse.REQUIRED) def test_concrete_float_as_template(self): - typ = confuse.as_template(2.) + typ = confuse.as_template(2.0) self.assertIsInstance(typ, confuse.Number) - self.assertEqual(typ.default, 2.) + self.assertEqual(typ.default, 2.0) def test_none_as_template(self): typ = confuse.as_template(None) @@ -172,8 +181,9 @@ def test_set_type_as_template(self): self.assertEqual(typ.default, confuse.REQUIRED) def test_other_type_as_template(self): - class MyClass(): + class MyClass: pass + typ = confuse.as_template(MyClass) self.assertIsInstance(typ, confuse.TypeTemplate) self.assertEqual(typ.typ, MyClass) @@ -182,112 +192,125 @@ class MyClass(): class StringTemplateTest(unittest.TestCase): def test_validate_string(self): - config = _root({'foo': 'bar'}) - valid = config.get({'foo': confuse.String()}) - self.assertEqual(valid['foo'], 'bar') + config = _root({"foo": "bar"}) + valid = config.get({"foo": confuse.String()}) + self.assertEqual(valid["foo"], "bar") def test_string_default_value(self): config = _root({}) - valid = config.get({'foo': confuse.String('baz')}) - self.assertEqual(valid['foo'], 'baz') + valid = config.get({"foo": confuse.String("baz")}) + self.assertEqual(valid["foo"], "baz") def test_pattern_matching(self): - config = _root({'foo': 'bar', 'baz': 'zab'}) - valid = config.get({'foo': confuse.String(pattern='^ba.$')}) - self.assertEqual(valid['foo'], 'bar') + config = _root({"foo": "bar", "baz": "zab"}) + valid = config.get({"foo": confuse.String(pattern="^ba.$")}) + self.assertEqual(valid["foo"], "bar") with self.assertRaises(confuse.ConfigValueError): - config.get({'baz': confuse.String(pattern='!')}) + config.get({"baz": confuse.String(pattern="!")}) def test_string_template_shortcut(self): - config = _root({'foo': 'bar'}) - valid = config.get({'foo': str}) - self.assertEqual(valid['foo'], 'bar') + config = _root({"foo": "bar"}) + valid = config.get({"foo": str}) + self.assertEqual(valid["foo"], "bar") def test_string_default_shortcut(self): config = _root({}) - valid = config.get({'foo': 'bar'}) - self.assertEqual(valid['foo'], 'bar') + valid = config.get({"foo": "bar"}) + self.assertEqual(valid["foo"], "bar") def test_check_string_type(self): - config = _root({'foo': 5}) + config = _root({"foo": 5}) with self.assertRaises(confuse.ConfigTypeError): - config.get({'foo': confuse.String()}) + config.get({"foo": confuse.String()}) class NumberTest(unittest.TestCase): def test_validate_int_as_number(self): - config = _root({'foo': 2}) - valid = config['foo'].get(confuse.Number()) + config = _root({"foo": 2}) + valid = config["foo"].get(confuse.Number()) self.assertIsInstance(valid, int) self.assertEqual(valid, 2) def test_validate_float_as_number(self): - config = _root({'foo': 3.0}) - valid = config['foo'].get(confuse.Number()) + config = _root({"foo": 3.0}) + valid = config["foo"].get(confuse.Number()) self.assertIsInstance(valid, float) self.assertEqual(valid, 3.0) def test_validate_string_as_number(self): - config = _root({'foo': 'bar'}) + config = _root({"foo": "bar"}) with self.assertRaises(confuse.ConfigTypeError): - config['foo'].get(confuse.Number()) + config["foo"].get(confuse.Number()) class ChoiceTest(unittest.TestCase): def test_validate_good_choice_in_list(self): - config = _root({'foo': 2}) - valid = config['foo'].get(confuse.Choice([1, 2, 4, 8, 16])) + config = _root({"foo": 2}) + valid = config["foo"].get(confuse.Choice([1, 2, 4, 8, 16])) self.assertEqual(valid, 2) def test_validate_bad_choice_in_list(self): - config = _root({'foo': 3}) + config = _root({"foo": 3}) with self.assertRaises(confuse.ConfigValueError): - config['foo'].get(confuse.Choice([1, 2, 4, 8, 16])) + config["foo"].get(confuse.Choice([1, 2, 4, 8, 16])) def test_validate_good_choice_in_dict(self): - config = _root({'foo': 2}) - valid = config['foo'].get(confuse.Choice({2: 'two', 4: 'four'})) - self.assertEqual(valid, 'two') + config = _root({"foo": 2}) + valid = config["foo"].get(confuse.Choice({2: "two", 4: "four"})) + self.assertEqual(valid, "two") def test_validate_bad_choice_in_dict(self): - config = _root({'foo': 3}) + config = _root({"foo": 3}) with self.assertRaises(confuse.ConfigValueError): - config['foo'].get(confuse.Choice({2: 'two', 4: 'four'})) + config["foo"].get(confuse.Choice({2: "two", 4: "four"})) class OneOfTest(unittest.TestCase): def test_default_value(self): config = _root({}) - valid = config['foo'].get(confuse.OneOf([], default='bar')) - self.assertEqual(valid, 'bar') + valid = config["foo"].get(confuse.OneOf([], default="bar")) + self.assertEqual(valid, "bar") def test_validate_good_choice_in_list(self): - config = _root({'foo': 2}) - valid = config['foo'].get(confuse.OneOf([ - confuse.String(), - confuse.Integer(), - ])) + config = _root({"foo": 2}) + valid = config["foo"].get( + confuse.OneOf( + [ + confuse.String(), + confuse.Integer(), + ] + ) + ) self.assertEqual(valid, 2) def test_validate_first_good_choice_in_list(self): - config = _root({'foo': 3.14}) - valid = config['foo'].get(confuse.OneOf([ - confuse.Integer(), - confuse.Number(), - ])) + config = _root({"foo": 3.14}) + valid = config["foo"].get( + confuse.OneOf( + [ + confuse.Integer(), + confuse.Number(), + ] + ) + ) self.assertEqual(valid, 3) def test_validate_no_choice_in_list(self): - config = _root({'foo': None}) + config = _root({"foo": None}) with self.assertRaises(confuse.ConfigValueError): - config['foo'].get(confuse.OneOf([ - confuse.String(), - confuse.Integer(), - ])) + config["foo"].get( + confuse.OneOf( + [ + confuse.String(), + confuse.Integer(), + ] + ) + ) def test_validate_bad_template(self): - class BadTemplate(): + class BadTemplate: pass + config = _root({}) with self.assertRaises(confuse.ConfigTemplateError): config.get(confuse.OneOf([BadTemplate()])) @@ -296,401 +319,388 @@ class BadTemplate(): class StrSeqTest(unittest.TestCase): def test_string_list(self): - config = _root({'foo': ['bar', 'baz']}) - valid = config['foo'].get(confuse.StrSeq()) - self.assertEqual(valid, ['bar', 'baz']) + config = _root({"foo": ["bar", "baz"]}) + valid = config["foo"].get(confuse.StrSeq()) + self.assertEqual(valid, ["bar", "baz"]) def test_string_tuple(self): - config = _root({'foo': ('bar', 'baz')}) - valid = config['foo'].get(confuse.StrSeq()) - self.assertEqual(valid, ['bar', 'baz']) + config = _root({"foo": ("bar", "baz")}) + valid = config["foo"].get(confuse.StrSeq()) + self.assertEqual(valid, ["bar", "baz"]) def test_whitespace_separated_string(self): - config = _root({'foo': 'bar baz'}) - valid = config['foo'].get(confuse.StrSeq()) - self.assertEqual(valid, ['bar', 'baz']) + config = _root({"foo": "bar baz"}) + valid = config["foo"].get(confuse.StrSeq()) + self.assertEqual(valid, ["bar", "baz"]) def test_invalid_type(self): - config = _root({'foo': 9}) + config = _root({"foo": 9}) with self.assertRaises(confuse.ConfigTypeError): - config['foo'].get(confuse.StrSeq()) + config["foo"].get(confuse.StrSeq()) def test_invalid_sequence_type(self): - config = _root({'foo': ['bar', 2126]}) + config = _root({"foo": ["bar", 2126]}) with self.assertRaises(confuse.ConfigTypeError): - config['foo'].get(confuse.StrSeq()) + config["foo"].get(confuse.StrSeq()) class FilenameTest(unittest.TestCase): def test_default_value(self): config = _root({}) - valid = config['foo'].get(confuse.Filename('foo/bar')) - self.assertEqual(valid, 'foo/bar') + valid = config["foo"].get(confuse.Filename("foo/bar")) + self.assertEqual(valid, "foo/bar") def test_default_none(self): config = _root({}) - valid = config['foo'].get(confuse.Filename(None)) + valid = config["foo"].get(confuse.Filename(None)) self.assertEqual(valid, None) def test_missing_required_value(self): config = _root({}) with self.assertRaises(confuse.NotFoundError): - config['foo'].get(confuse.Filename()) + config["foo"].get(confuse.Filename()) def test_filename_relative_to_working_dir(self): - config = _root({'foo': 'bar'}) - valid = config['foo'].get(confuse.Filename(cwd='/dev/null')) - self.assertEqual(valid, os.path.realpath('/dev/null/bar')) + config = _root({"foo": "bar"}) + valid = config["foo"].get(confuse.Filename(cwd="/dev/null")) + self.assertEqual(valid, os.path.realpath("/dev/null/bar")) def test_filename_relative_to_sibling(self): - config = _root({'foo': '/', 'bar': 'baz'}) - valid = config.get({ - 'foo': confuse.Filename(), - 'bar': confuse.Filename(relative_to='foo') - }) - self.assertEqual(valid.foo, os.path.realpath('/')) - self.assertEqual(valid.bar, os.path.realpath('/baz')) + config = _root({"foo": "/", "bar": "baz"}) + valid = config.get( + {"foo": confuse.Filename(), "bar": confuse.Filename(relative_to="foo")} + ) + self.assertEqual(valid.foo, os.path.realpath("/")) + self.assertEqual(valid.bar, os.path.realpath("/baz")) def test_filename_working_dir_overrides_sibling(self): - config = _root({'foo': 'bar'}) - valid = config.get({ - 'foo': confuse.Filename(cwd='/dev/null', relative_to='baz') - }) - self.assertEqual(valid.foo, os.path.realpath('/dev/null/bar')) + config = _root({"foo": "bar"}) + valid = config.get( + {"foo": confuse.Filename(cwd="/dev/null", relative_to="baz")} + ) + self.assertEqual(valid.foo, os.path.realpath("/dev/null/bar")) def test_filename_relative_to_sibling_with_recursion(self): - config = _root({'foo': '/', 'bar': 'r', 'baz': 'z'}) + config = _root({"foo": "/", "bar": "r", "baz": "z"}) with self.assertRaises(confuse.ConfigTemplateError): - config.get({ - 'foo': confuse.Filename(relative_to='bar'), - 'bar': confuse.Filename(relative_to='baz'), - 'baz': confuse.Filename(relative_to='foo') - }) + config.get( + { + "foo": confuse.Filename(relative_to="bar"), + "bar": confuse.Filename(relative_to="baz"), + "baz": confuse.Filename(relative_to="foo"), + } + ) def test_filename_relative_to_self(self): - config = _root({'foo': 'bar'}) + config = _root({"foo": "bar"}) with self.assertRaises(confuse.ConfigTemplateError): - config.get({ - 'foo': confuse.Filename(relative_to='foo') - }) + config.get({"foo": confuse.Filename(relative_to="foo")}) def test_filename_relative_to_sibling_needs_siblings(self): - config = _root({'foo': 'bar'}) + config = _root({"foo": "bar"}) with self.assertRaises(confuse.ConfigTemplateError): - config['foo'].get(confuse.Filename(relative_to='bar')) + config["foo"].get(confuse.Filename(relative_to="bar")) def test_filename_relative_to_sibling_needs_template(self): - config = _root({'foo': '/', 'bar': 'baz'}) + config = _root({"foo": "/", "bar": "baz"}) with self.assertRaises(confuse.ConfigTemplateError): - config.get({ - 'bar': confuse.Filename(relative_to='foo') - }) + config.get({"bar": confuse.Filename(relative_to="foo")}) def test_filename_with_non_file_source(self): - config = _root({'foo': 'foo/bar'}) - valid = config['foo'].get(confuse.Filename()) - self.assertEqual(valid, os.path.join(os.getcwd(), 'foo', 'bar')) + config = _root({"foo": "foo/bar"}) + valid = config["foo"].get(confuse.Filename()) + self.assertEqual(valid, os.path.join(os.getcwd(), "foo", "bar")) def test_filename_with_file_source(self): - source = confuse.ConfigSource({'foo': 'foo/bar'}, - filename='/baz/config.yaml') + source = confuse.ConfigSource({"foo": "foo/bar"}, filename="/baz/config.yaml") config = _root(source) - config.config_dir = lambda: '/config/path' - valid = config['foo'].get(confuse.Filename()) - self.assertEqual(valid, os.path.realpath('/config/path/foo/bar')) + config.config_dir = lambda: "/config/path" + valid = config["foo"].get(confuse.Filename()) + self.assertEqual(valid, os.path.realpath("/config/path/foo/bar")) def test_filename_with_default_source(self): - source = confuse.ConfigSource({'foo': 'foo/bar'}, - filename='/baz/config.yaml', - default=True) + source = confuse.ConfigSource( + {"foo": "foo/bar"}, filename="/baz/config.yaml", default=True + ) config = _root(source) - config.config_dir = lambda: '/config/path' - valid = config['foo'].get(confuse.Filename()) - self.assertEqual(valid, os.path.realpath('/config/path/foo/bar')) + config.config_dir = lambda: "/config/path" + valid = config["foo"].get(confuse.Filename()) + self.assertEqual(valid, os.path.realpath("/config/path/foo/bar")) def test_filename_use_config_source_dir(self): - source = confuse.ConfigSource({'foo': 'foo/bar'}, - filename='/baz/config.yaml', - base_for_paths=True) + source = confuse.ConfigSource( + {"foo": "foo/bar"}, filename="/baz/config.yaml", base_for_paths=True + ) config = _root(source) - config.config_dir = lambda: '/config/path' - valid = config['foo'].get(confuse.Filename()) - self.assertEqual(valid, os.path.realpath('/baz/foo/bar')) + config.config_dir = lambda: "/config/path" + valid = config["foo"].get(confuse.Filename()) + self.assertEqual(valid, os.path.realpath("/baz/foo/bar")) def test_filename_in_source_dir(self): - source = confuse.ConfigSource({'foo': 'foo/bar'}, - filename='/baz/config.yaml') + source = confuse.ConfigSource({"foo": "foo/bar"}, filename="/baz/config.yaml") config = _root(source) - config.config_dir = lambda: '/config/path' - valid = config['foo'].get(confuse.Filename(in_source_dir=True)) - self.assertEqual(valid, os.path.realpath('/baz/foo/bar')) + config.config_dir = lambda: "/config/path" + valid = config["foo"].get(confuse.Filename(in_source_dir=True)) + self.assertEqual(valid, os.path.realpath("/baz/foo/bar")) def test_filename_in_source_dir_overrides_in_app_dir(self): - source = confuse.ConfigSource({'foo': 'foo/bar'}, - filename='/baz/config.yaml') + source = confuse.ConfigSource({"foo": "foo/bar"}, filename="/baz/config.yaml") config = _root(source) - config.config_dir = lambda: '/config/path' - valid = config['foo'].get(confuse.Filename(in_source_dir=True, - in_app_dir=True)) - self.assertEqual(valid, os.path.realpath('/baz/foo/bar')) + config.config_dir = lambda: "/config/path" + valid = config["foo"].get(confuse.Filename(in_source_dir=True, in_app_dir=True)) + self.assertEqual(valid, os.path.realpath("/baz/foo/bar")) def test_filename_in_app_dir_non_file_source(self): - source = confuse.ConfigSource({'foo': 'foo/bar'}) + source = confuse.ConfigSource({"foo": "foo/bar"}) config = _root(source) - config.config_dir = lambda: '/config/path' - valid = config['foo'].get(confuse.Filename(in_app_dir=True)) - self.assertEqual(valid, os.path.realpath('/config/path/foo/bar')) + config.config_dir = lambda: "/config/path" + valid = config["foo"].get(confuse.Filename(in_app_dir=True)) + self.assertEqual(valid, os.path.realpath("/config/path/foo/bar")) def test_filename_in_app_dir_overrides_config_source_dir(self): - source = confuse.ConfigSource({'foo': 'foo/bar'}, - filename='/baz/config.yaml', - base_for_paths=True) + source = confuse.ConfigSource( + {"foo": "foo/bar"}, filename="/baz/config.yaml", base_for_paths=True + ) config = _root(source) - config.config_dir = lambda: '/config/path' - valid = config['foo'].get(confuse.Filename(in_app_dir=True)) - self.assertEqual(valid, os.path.realpath('/config/path/foo/bar')) + config.config_dir = lambda: "/config/path" + valid = config["foo"].get(confuse.Filename(in_app_dir=True)) + self.assertEqual(valid, os.path.realpath("/config/path/foo/bar")) def test_filename_wrong_type(self): - config = _root({'foo': 8}) + config = _root({"foo": 8}) with self.assertRaises(confuse.ConfigTypeError): - config['foo'].get(confuse.Filename()) + config["foo"].get(confuse.Filename()) class PathTest(unittest.TestCase): def test_path_value(self): import pathlib - config = _root({'foo': 'foo/bar'}) - valid = config['foo'].get(confuse.Path()) - self.assertEqual(valid, pathlib.Path(os.path.abspath('foo/bar'))) + + config = _root({"foo": "foo/bar"}) + valid = config["foo"].get(confuse.Path()) + self.assertEqual(valid, pathlib.Path(os.path.abspath("foo/bar"))) def test_default_value(self): import pathlib + config = _root({}) - valid = config['foo'].get(confuse.Path('foo/bar')) - self.assertEqual(valid, pathlib.Path('foo/bar')) + valid = config["foo"].get(confuse.Path("foo/bar")) + self.assertEqual(valid, pathlib.Path("foo/bar")) def test_default_none(self): config = _root({}) - valid = config['foo'].get(confuse.Path(None)) + valid = config["foo"].get(confuse.Path(None)) self.assertEqual(valid, None) def test_missing_required_value(self): config = _root({}) with self.assertRaises(confuse.NotFoundError): - config['foo'].get(confuse.Path()) + config["foo"].get(confuse.Path()) class BaseTemplateTest(unittest.TestCase): def test_base_template_accepts_any_value(self): - config = _root({'foo': 4.2}) - valid = config['foo'].get(confuse.Template()) + config = _root({"foo": 4.2}) + valid = config["foo"].get(confuse.Template()) self.assertEqual(valid, 4.2) def test_base_template_required(self): config = _root({}) with self.assertRaises(confuse.NotFoundError): - config['foo'].get(confuse.Template()) + config["foo"].get(confuse.Template()) def test_base_template_with_default(self): config = _root({}) - valid = config['foo'].get(confuse.Template('bar')) - self.assertEqual(valid, 'bar') + valid = config["foo"].get(confuse.Template("bar")) + self.assertEqual(valid, "bar") class TypeTemplateTest(unittest.TestCase): def test_correct_type(self): - config = _root({'foo': set()}) - valid = config['foo'].get(confuse.TypeTemplate(set)) + config = _root({"foo": set()}) + valid = config["foo"].get(confuse.TypeTemplate(set)) self.assertEqual(valid, set()) def test_incorrect_type(self): - config = _root({'foo': dict()}) + config = _root({"foo": dict()}) with self.assertRaises(confuse.ConfigTypeError): - config['foo'].get(confuse.TypeTemplate(set)) + config["foo"].get(confuse.TypeTemplate(set)) def test_missing_required_value(self): config = _root({}) with self.assertRaises(confuse.NotFoundError): - config['foo'].get(confuse.TypeTemplate(set)) + config["foo"].get(confuse.TypeTemplate(set)) def test_default_value(self): config = _root({}) - valid = config['foo'].get(confuse.TypeTemplate(set, set([1, 2]))) + valid = config["foo"].get(confuse.TypeTemplate(set, set([1, 2]))) self.assertEqual(valid, set([1, 2])) class SequenceTest(unittest.TestCase): def test_int_list(self): - config = _root({'foo': [1, 2, 3]}) - valid = config['foo'].get(confuse.Sequence(int)) + config = _root({"foo": [1, 2, 3]}) + valid = config["foo"].get(confuse.Sequence(int)) self.assertEqual(valid, [1, 2, 3]) def test_dict_list(self): - config = _root({'foo': [{'bar': 1, 'baz': 2}, {'bar': 3, 'baz': 4}]}) - valid = config['foo'].get(confuse.Sequence( - {'bar': int, 'baz': int} - )) - self.assertEqual(valid, [ - {'bar': 1, 'baz': 2}, {'bar': 3, 'baz': 4} - ]) + config = _root({"foo": [{"bar": 1, "baz": 2}, {"bar": 3, "baz": 4}]}) + valid = config["foo"].get(confuse.Sequence({"bar": int, "baz": int})) + self.assertEqual(valid, [{"bar": 1, "baz": 2}, {"bar": 3, "baz": 4}]) def test_invalid_item(self): - config = _root({'foo': [{'bar': 1, 'baz': 2}, {'bar': 3, 'bak': 4}]}) + config = _root({"foo": [{"bar": 1, "baz": 2}, {"bar": 3, "bak": 4}]}) with self.assertRaises(confuse.NotFoundError): - config['foo'].get(confuse.Sequence( - {'bar': int, 'baz': int} - )) + config["foo"].get(confuse.Sequence({"bar": int, "baz": int})) def test_wrong_type(self): - config = _root({'foo': {'one': 1, 'two': 2, 'three': 3}}) + config = _root({"foo": {"one": 1, "two": 2, "three": 3}}) with self.assertRaises(confuse.ConfigTypeError): - config['foo'].get(confuse.Sequence(int)) + config["foo"].get(confuse.Sequence(int)) def test_missing(self): - config = _root({'foo': [1, 2, 3]}) - valid = config['bar'].get(confuse.Sequence(int)) + config = _root({"foo": [1, 2, 3]}) + valid = config["bar"].get(confuse.Sequence(int)) self.assertEqual(valid, []) class MappingValuesTest(unittest.TestCase): def test_int_dict(self): - config = _root({'foo': {'one': 1, 'two': 2, 'three': 3}}) - valid = config['foo'].get(confuse.MappingValues(int)) - self.assertEqual(valid, {'one': 1, 'two': 2, 'three': 3}) + config = _root({"foo": {"one": 1, "two": 2, "three": 3}}) + valid = config["foo"].get(confuse.MappingValues(int)) + self.assertEqual(valid, {"one": 1, "two": 2, "three": 3}) def test_dict_dict(self): - config = _root({'foo': {'first': {'bar': 1, 'baz': 2}, - 'second': {'bar': 3, 'baz': 4}}}) - valid = config['foo'].get(confuse.MappingValues( - {'bar': int, 'baz': int} - )) - self.assertEqual(valid, { - 'first': {'bar': 1, 'baz': 2}, - 'second': {'bar': 3, 'baz': 4}, - }) + config = _root( + {"foo": {"first": {"bar": 1, "baz": 2}, "second": {"bar": 3, "baz": 4}}} + ) + valid = config["foo"].get(confuse.MappingValues({"bar": int, "baz": int})) + self.assertEqual( + valid, + { + "first": {"bar": 1, "baz": 2}, + "second": {"bar": 3, "baz": 4}, + }, + ) def test_invalid_item(self): - config = _root({'foo': {'first': {'bar': 1, 'baz': 2}, - 'second': {'bar': 3, 'bak': 4}}}) + config = _root( + {"foo": {"first": {"bar": 1, "baz": 2}, "second": {"bar": 3, "bak": 4}}} + ) with self.assertRaises(confuse.NotFoundError): - config['foo'].get(confuse.MappingValues( - {'bar': int, 'baz': int} - )) + config["foo"].get(confuse.MappingValues({"bar": int, "baz": int})) def test_wrong_type(self): - config = _root({'foo': [1, 2, 3]}) + config = _root({"foo": [1, 2, 3]}) with self.assertRaises(confuse.ConfigTypeError): - config['foo'].get(confuse.MappingValues(int)) + config["foo"].get(confuse.MappingValues(int)) def test_missing(self): - config = _root({'foo': {'one': 1, 'two': 2, 'three': 3}}) - valid = config['bar'].get(confuse.MappingValues(int)) + config = _root({"foo": {"one": 1, "two": 2, "three": 3}}) + valid = config["bar"].get(confuse.MappingValues(int)) self.assertEqual(valid, {}) class OptionalTest(unittest.TestCase): def test_optional_string_valid_type(self): - config = _root({'foo': 'bar'}) - valid = config['foo'].get(confuse.Optional(confuse.String())) - self.assertEqual(valid, 'bar') + config = _root({"foo": "bar"}) + valid = config["foo"].get(confuse.Optional(confuse.String())) + self.assertEqual(valid, "bar") def test_optional_string_invalid_type(self): - config = _root({'foo': 5}) + config = _root({"foo": 5}) with self.assertRaises(confuse.ConfigTypeError): - config['foo'].get(confuse.Optional(confuse.String())) + config["foo"].get(confuse.Optional(confuse.String())) def test_optional_string_null(self): - config = _root({'foo': None}) - valid = config['foo'].get(confuse.Optional(confuse.String())) + config = _root({"foo": None}) + valid = config["foo"].get(confuse.Optional(confuse.String())) self.assertIsNone(valid) def test_optional_string_null_default_value(self): - config = _root({'foo': None}) - valid = config['foo'].get(confuse.Optional(confuse.String(), 'baz')) - self.assertEqual(valid, 'baz') + config = _root({"foo": None}) + valid = config["foo"].get(confuse.Optional(confuse.String(), "baz")) + self.assertEqual(valid, "baz") def test_optional_string_null_string_provides_default(self): - config = _root({'foo': None}) - valid = config['foo'].get(confuse.Optional(confuse.String('baz'))) - self.assertEqual(valid, 'baz') + config = _root({"foo": None}) + valid = config["foo"].get(confuse.Optional(confuse.String("baz"))) + self.assertEqual(valid, "baz") def test_optional_string_null_string_default_override(self): - config = _root({'foo': None}) - valid = config['foo'].get(confuse.Optional(confuse.String('baz'), - default='bar')) - self.assertEqual(valid, 'bar') + config = _root({"foo": None}) + valid = config["foo"].get( + confuse.Optional(confuse.String("baz"), default="bar") + ) + self.assertEqual(valid, "bar") def test_optional_string_allow_missing_no_explicit_default(self): config = _root({}) - valid = config['foo'].get(confuse.Optional(confuse.String())) + valid = config["foo"].get(confuse.Optional(confuse.String())) self.assertIsNone(valid) def test_optional_string_allow_missing_default_value(self): config = _root({}) - valid = config['foo'].get(confuse.Optional(confuse.String(), 'baz')) - self.assertEqual(valid, 'baz') + valid = config["foo"].get(confuse.Optional(confuse.String(), "baz")) + self.assertEqual(valid, "baz") def test_optional_string_missing_not_allowed(self): config = _root({}) with self.assertRaises(confuse.NotFoundError): - config['foo'].get( - confuse.Optional(confuse.String(), allow_missing=False) - ) + config["foo"].get(confuse.Optional(confuse.String(), allow_missing=False)) def test_optional_string_null_missing_not_allowed(self): - config = _root({'foo': None}) - valid = config['foo'].get( + config = _root({"foo": None}) + valid = config["foo"].get( confuse.Optional(confuse.String(), allow_missing=False) ) self.assertIsNone(valid) def test_optional_mapping_template_valid(self): - config = _root({'foo': {'bar': 5, 'baz': 'bak'}}) - template = {'bar': confuse.Integer(), 'baz': confuse.String()} - valid = config.get({'foo': confuse.Optional(template)}) - self.assertEqual(valid['foo']['bar'], 5) - self.assertEqual(valid['foo']['baz'], 'bak') + config = _root({"foo": {"bar": 5, "baz": "bak"}}) + template = {"bar": confuse.Integer(), "baz": confuse.String()} + valid = config.get({"foo": confuse.Optional(template)}) + self.assertEqual(valid["foo"]["bar"], 5) + self.assertEqual(valid["foo"]["baz"], "bak") def test_optional_mapping_template_invalid(self): - config = _root({'foo': {'bar': 5, 'baz': 10}}) - template = {'bar': confuse.Integer(), 'baz': confuse.String()} + config = _root({"foo": {"bar": 5, "baz": 10}}) + template = {"bar": confuse.Integer(), "baz": confuse.String()} with self.assertRaises(confuse.ConfigTypeError): - config.get({'foo': confuse.Optional(template)}) + config.get({"foo": confuse.Optional(template)}) def test_optional_mapping_template_null(self): - config = _root({'foo': None}) - template = {'bar': confuse.Integer(), 'baz': confuse.String()} - valid = config.get({'foo': confuse.Optional(template)}) - self.assertIsNone(valid['foo']) + config = _root({"foo": None}) + template = {"bar": confuse.Integer(), "baz": confuse.String()} + valid = config.get({"foo": confuse.Optional(template)}) + self.assertIsNone(valid["foo"]) def test_optional_mapping_template_null_default_value(self): - config = _root({'foo': None}) - template = {'bar': confuse.Integer(), 'baz': confuse.String()} - valid = config.get({'foo': confuse.Optional(template, {})}) - self.assertIsInstance(valid['foo'], dict) + config = _root({"foo": None}) + template = {"bar": confuse.Integer(), "baz": confuse.String()} + valid = config.get({"foo": confuse.Optional(template, {})}) + self.assertIsInstance(valid["foo"], dict) def test_optional_mapping_template_allow_missing_no_explicit_default(self): config = _root({}) - template = {'bar': confuse.Integer(), 'baz': confuse.String()} - valid = config.get({'foo': confuse.Optional(template)}) - self.assertIsNone(valid['foo']) + template = {"bar": confuse.Integer(), "baz": confuse.String()} + valid = config.get({"foo": confuse.Optional(template)}) + self.assertIsNone(valid["foo"]) def test_optional_mapping_template_allow_missing_default_value(self): config = _root({}) - template = {'bar': confuse.Integer(), 'baz': confuse.String()} - valid = config.get({'foo': confuse.Optional(template, {})}) - self.assertIsInstance(valid['foo'], dict) + template = {"bar": confuse.Integer(), "baz": confuse.String()} + valid = config.get({"foo": confuse.Optional(template, {})}) + self.assertIsInstance(valid["foo"], dict) def test_optional_mapping_template_missing_not_allowed(self): config = _root({}) - template = {'bar': confuse.Integer(), 'baz': confuse.String()} + template = {"bar": confuse.Integer(), "baz": confuse.String()} with self.assertRaises(confuse.NotFoundError): - config.get({'foo': confuse.Optional(template, - allow_missing=False)}) + config.get({"foo": confuse.Optional(template, allow_missing=False)}) def test_optional_mapping_template_null_missing_not_allowed(self): - config = _root({'foo': None}) - template = {'bar': confuse.Integer(), 'baz': confuse.String()} - valid = config.get({'foo': confuse.Optional(template, - allow_missing=False)}) - self.assertIsNone(valid['foo']) + config = _root({"foo": None}) + template = {"bar": confuse.Integer(), "baz": confuse.String()} + valid = config.get({"foo": confuse.Optional(template, allow_missing=False)}) + self.assertIsNone(valid["foo"]) diff --git a/test/test_validation.py b/test/test_validation.py index bb212f5..f31127a 100644 --- a/test/test_validation.py +++ b/test/test_validation.py @@ -1,151 +1,145 @@ -import confuse import enum import os import unittest + +import confuse + from . import _root class TypeCheckTest(unittest.TestCase): def test_str_type_correct(self): - config = _root({'foo': 'bar'}) - value = config['foo'].get(str) - self.assertEqual(value, 'bar') + config = _root({"foo": "bar"}) + value = config["foo"].get(str) + self.assertEqual(value, "bar") def test_str_type_incorrect(self): - config = _root({'foo': 2}) + config = _root({"foo": 2}) with self.assertRaises(confuse.ConfigTypeError): - config['foo'].get(str) + config["foo"].get(str) def test_int_type_correct(self): - config = _root({'foo': 2}) - value = config['foo'].get(int) + config = _root({"foo": 2}) + value = config["foo"].get(int) self.assertEqual(value, 2) def test_int_type_incorrect(self): - config = _root({'foo': 'bar'}) + config = _root({"foo": "bar"}) with self.assertRaises(confuse.ConfigTypeError): - config['foo'].get(int) + config["foo"].get(int) class BuiltInValidatorTest(unittest.TestCase): def test_as_filename_with_non_file_source(self): - config = _root({'foo': 'foo/bar'}) - value = config['foo'].as_filename() - self.assertEqual(value, os.path.join(os.getcwd(), 'foo', 'bar')) + config = _root({"foo": "foo/bar"}) + value = config["foo"].as_filename() + self.assertEqual(value, os.path.join(os.getcwd(), "foo", "bar")) def test_as_filename_with_file_source(self): - source = confuse.ConfigSource({'foo': 'foo/bar'}, - filename='/baz/config.yaml') + source = confuse.ConfigSource({"foo": "foo/bar"}, filename="/baz/config.yaml") config = _root(source) - config.config_dir = lambda: '/config/path' - value = config['foo'].as_filename() - self.assertEqual(value, os.path.realpath('/config/path/foo/bar')) + config.config_dir = lambda: "/config/path" + value = config["foo"].as_filename() + self.assertEqual(value, os.path.realpath("/config/path/foo/bar")) def test_as_filename_with_default_source(self): - source = confuse.ConfigSource({'foo': 'foo/bar'}, - filename='/baz/config.yaml', - default=True) + source = confuse.ConfigSource( + {"foo": "foo/bar"}, filename="/baz/config.yaml", default=True + ) config = _root(source) - config.config_dir = lambda: '/config/path' - value = config['foo'].as_filename() - self.assertEqual(value, os.path.realpath('/config/path/foo/bar')) + config.config_dir = lambda: "/config/path" + value = config["foo"].as_filename() + self.assertEqual(value, os.path.realpath("/config/path/foo/bar")) def test_as_filename_wrong_type(self): - config = _root({'foo': None}) + config = _root({"foo": None}) with self.assertRaises(confuse.ConfigTypeError): - config['foo'].as_filename() + config["foo"].as_filename() def test_as_path(self): - config = _root({'foo': 'foo/bar'}) - path = os.path.join(os.getcwd(), 'foo', 'bar') + config = _root({"foo": "foo/bar"}) + path = os.path.join(os.getcwd(), "foo", "bar") try: import pathlib except ImportError: with self.assertRaises(ImportError): - value = config['foo'].as_path() + value = config["foo"].as_path() else: - value = config['foo'].as_path() + value = config["foo"].as_path() path = pathlib.Path(path) self.assertEqual(value, path) def test_as_choice_correct(self): - config = _root({'foo': 'bar'}) - value = config['foo'].as_choice(['foo', 'bar', 'baz']) - self.assertEqual(value, 'bar') + config = _root({"foo": "bar"}) + value = config["foo"].as_choice(["foo", "bar", "baz"]) + self.assertEqual(value, "bar") def test_as_choice_error(self): - config = _root({'foo': 'bar'}) + config = _root({"foo": "bar"}) with self.assertRaises(confuse.ConfigValueError): - config['foo'].as_choice(['foo', 'baz']) + config["foo"].as_choice(["foo", "baz"]) def test_as_choice_with_dict(self): - config = _root({'foo': 'bar'}) - res = config['foo'].as_choice({ - 'bar': 'baz', - 'x': 'y', - }) - self.assertEqual(res, 'baz') + config = _root({"foo": "bar"}) + res = config["foo"].as_choice( + { + "bar": "baz", + "x": "y", + } + ) + self.assertEqual(res, "baz") def test_as_choice_with_enum(self): class Foobar(enum.Enum): - Foo = 'bar' + Foo = "bar" - config = _root({'foo': Foobar.Foo.value}) - res = config['foo'].as_choice(Foobar) + config = _root({"foo": Foobar.Foo.value}) + res = config["foo"].as_choice(Foobar) self.assertEqual(res, Foobar.Foo) def test_as_choice_with_enum_error(self): class Foobar(enum.Enum): - Foo = 'bar' + Foo = "bar" - config = _root({'foo': 'foo'}) + config = _root({"foo": "foo"}) with self.assertRaises(confuse.ConfigValueError): - config['foo'].as_choice(Foobar) + config["foo"].as_choice(Foobar) def test_as_number_float(self): - config = _root({'f': 1.0}) - config['f'].as_number() + config = _root({"f": 1.0}) + config["f"].as_number() def test_as_number_int(self): - config = _root({'i': 2}) - config['i'].as_number() + config = _root({"i": 2}) + config["i"].as_number() def test_as_number_string(self): - config = _root({'s': 'a'}) + config = _root({"s": "a"}) with self.assertRaises(confuse.ConfigTypeError): - config['s'].as_number() + config["s"].as_number() def test_as_str_seq_str(self): - config = _root({'k': 'a b c'}) - self.assertEqual( - config['k'].as_str_seq(), - ['a', 'b', 'c'] - ) + config = _root({"k": "a b c"}) + self.assertEqual(config["k"].as_str_seq(), ["a", "b", "c"]) def test_as_str_seq_list(self): - config = _root({'k': ['a b', 'c']}) - self.assertEqual( - config['k'].as_str_seq(), - ['a b', 'c'] - ) + config = _root({"k": ["a b", "c"]}) + self.assertEqual(config["k"].as_str_seq(), ["a b", "c"]) def test_as_str(self): - config = _root({'s': 'foo'}) - config['s'].as_str() + config = _root({"s": "foo"}) + config["s"].as_str() def test_as_str_non_string(self): - config = _root({'f': 1.0}) + config = _root({"f": 1.0}) with self.assertRaises(confuse.ConfigTypeError): - config['f'].as_str() + config["f"].as_str() def test_as_str_expanded(self): - config = _root({'s': '${CONFUSE_TEST_VAR}/bar'}) - os.environ["CONFUSE_TEST_VAR"] = 'foo' - self.assertEqual(config['s'].as_str_expanded(), 'foo/bar') + config = _root({"s": "${CONFUSE_TEST_VAR}/bar"}) + os.environ["CONFUSE_TEST_VAR"] = "foo" + self.assertEqual(config["s"].as_str_expanded(), "foo/bar") def test_as_pairs(self): - config = _root({'k': [{'a': 'A'}, 'b', ['c', 'C']]}) - self.assertEqual( - [('a', 'A'), ('b', None), ('c', 'C')], - config['k'].as_pairs() - ) + config = _root({"k": [{"a": "A"}, "b", ["c", "C"]]}) + self.assertEqual([("a", "A"), ("b", None), ("c", "C")], config["k"].as_pairs()) diff --git a/test/test_views.py b/test/test_views.py index 32f73ca..6f5d6b0 100644 --- a/test/test_views.py +++ b/test/test_views.py @@ -1,120 +1,122 @@ -import confuse import unittest + +import confuse + from . import _root class SingleSourceTest(unittest.TestCase): def test_dict_access(self): - config = _root({'foo': 'bar'}) - value = config['foo'].get() - self.assertEqual(value, 'bar') + config = _root({"foo": "bar"}) + value = config["foo"].get() + self.assertEqual(value, "bar") def test_list_access(self): - config = _root({'foo': ['bar', 'baz']}) - value = config['foo'][1].get() - self.assertEqual(value, 'baz') + config = _root({"foo": ["bar", "baz"]}) + value = config["foo"][1].get() + self.assertEqual(value, "baz") def test_missing_key(self): - config = _root({'foo': 'bar'}) + config = _root({"foo": "bar"}) with self.assertRaises(confuse.NotFoundError): - config['baz'].get() + config["baz"].get() def test_missing_index(self): - config = _root({'l': ['foo', 'bar']}) + config = _root({"l": ["foo", "bar"]}) with self.assertRaises(confuse.NotFoundError): - config['l'][5].get() + config["l"][5].get() def test_dict_iter(self): - config = _root({'foo': 'bar', 'baz': 'qux'}) + config = _root({"foo": "bar", "baz": "qux"}) keys = [key for key in config] - self.assertEqual(set(keys), set(['foo', 'baz'])) + self.assertEqual(set(keys), set(["foo", "baz"])) def test_list_iter(self): - config = _root({'l': ['foo', 'bar']}) - items = [subview.get() for subview in config['l']] - self.assertEqual(items, ['foo', 'bar']) + config = _root({"l": ["foo", "bar"]}) + items = [subview.get() for subview in config["l"]] + self.assertEqual(items, ["foo", "bar"]) def test_int_iter(self): - config = _root({'n': 2}) + config = _root({"n": 2}) with self.assertRaises(confuse.ConfigTypeError): - [item for item in config['n']] + [item for item in config["n"]] def test_dict_keys(self): - config = _root({'foo': 'bar', 'baz': 'qux'}) + config = _root({"foo": "bar", "baz": "qux"}) keys = config.keys() - self.assertEqual(set(keys), set(['foo', 'baz'])) + self.assertEqual(set(keys), set(["foo", "baz"])) def test_dict_values(self): - config = _root({'foo': 'bar', 'baz': 'qux'}) + config = _root({"foo": "bar", "baz": "qux"}) values = [value.get() for value in config.values()] - self.assertEqual(set(values), set(['bar', 'qux'])) + self.assertEqual(set(values), set(["bar", "qux"])) def test_dict_items(self): - config = _root({'foo': 'bar', 'baz': 'qux'}) + config = _root({"foo": "bar", "baz": "qux"}) items = [(key, value.get()) for (key, value) in config.items()] - self.assertEqual(set(items), set([('foo', 'bar'), ('baz', 'qux')])) + self.assertEqual(set(items), set([("foo", "bar"), ("baz", "qux")])) def test_list_keys_error(self): - config = _root({'l': ['foo', 'bar']}) + config = _root({"l": ["foo", "bar"]}) with self.assertRaises(confuse.ConfigTypeError): - config['l'].keys() + config["l"].keys() def test_list_sequence(self): - config = _root({'l': ['foo', 'bar']}) - items = [item.get() for item in config['l'].sequence()] - self.assertEqual(items, ['foo', 'bar']) + config = _root({"l": ["foo", "bar"]}) + items = [item.get() for item in config["l"].sequence()] + self.assertEqual(items, ["foo", "bar"]) def test_dict_sequence_error(self): - config = _root({'foo': 'bar', 'baz': 'qux'}) + config = _root({"foo": "bar", "baz": "qux"}) with self.assertRaises(confuse.ConfigTypeError): list(config.sequence()) def test_dict_contents(self): - config = _root({'foo': 'bar', 'baz': 'qux'}) + config = _root({"foo": "bar", "baz": "qux"}) contents = config.all_contents() - self.assertEqual(set(contents), set(['foo', 'baz'])) + self.assertEqual(set(contents), set(["foo", "baz"])) def test_list_contents(self): - config = _root({'l': ['foo', 'bar']}) - contents = config['l'].all_contents() - self.assertEqual(list(contents), ['foo', 'bar']) + config = _root({"l": ["foo", "bar"]}) + contents = config["l"].all_contents() + self.assertEqual(list(contents), ["foo", "bar"]) def test_int_contents(self): - config = _root({'n': 2}) + config = _root({"n": 2}) with self.assertRaises(confuse.ConfigTypeError): - list(config['n'].all_contents()) + list(config["n"].all_contents()) class ConverstionTest(unittest.TestCase): def test_str_conversion_from_str(self): - config = _root({'foo': 'bar'}) - value = str(config['foo']) - self.assertEqual(value, 'bar') + config = _root({"foo": "bar"}) + value = str(config["foo"]) + self.assertEqual(value, "bar") def test_str_conversion_from_int(self): - config = _root({'foo': 2}) - value = str(config['foo']) - self.assertEqual(value, '2') + config = _root({"foo": 2}) + value = str(config["foo"]) + self.assertEqual(value, "2") def test_bool_conversion_from_bool(self): - config = _root({'foo': True}) - value = bool(config['foo']) + config = _root({"foo": True}) + value = bool(config["foo"]) self.assertEqual(value, True) def test_bool_conversion_from_int(self): - config = _root({'foo': 0}) - value = bool(config['foo']) + config = _root({"foo": 0}) + value = bool(config["foo"]) self.assertEqual(value, False) class NameTest(unittest.TestCase): def test_root_name(self): config = _root() - self.assertEqual(config.name, 'root') + self.assertEqual(config.name, "root") def test_string_access_name(self): config = _root() - name = config['foo'].name + name = config["foo"].name self.assertEqual(name, "foo") def test_int_access_name(self): @@ -124,140 +126,140 @@ def test_int_access_name(self): def test_nested_access_name(self): config = _root() - name = config[5]['foo']['bar'][20].name + name = config[5]["foo"]["bar"][20].name self.assertEqual(name, "#5.foo.bar#20") class MultipleSourceTest(unittest.TestCase): def test_dict_access_shadowed(self): - config = _root({'foo': 'bar'}, {'foo': 'baz'}) - value = config['foo'].get() - self.assertEqual(value, 'bar') + config = _root({"foo": "bar"}, {"foo": "baz"}) + value = config["foo"].get() + self.assertEqual(value, "bar") def test_dict_access_fall_through(self): - config = _root({'qux': 'bar'}, {'foo': 'baz'}) - value = config['foo'].get() - self.assertEqual(value, 'baz') + config = _root({"qux": "bar"}, {"foo": "baz"}) + value = config["foo"].get() + self.assertEqual(value, "baz") def test_dict_access_missing(self): - config = _root({'qux': 'bar'}, {'foo': 'baz'}) + config = _root({"qux": "bar"}, {"foo": "baz"}) with self.assertRaises(confuse.NotFoundError): - config['fred'].get() + config["fred"].get() def test_list_access_shadowed(self): - config = _root({'l': ['a', 'b']}, {'l': ['c', 'd', 'e']}) - value = config['l'][1].get() - self.assertEqual(value, 'b') + config = _root({"l": ["a", "b"]}, {"l": ["c", "d", "e"]}) + value = config["l"][1].get() + self.assertEqual(value, "b") def test_list_access_fall_through(self): - config = _root({'l': ['a', 'b']}, {'l': ['c', 'd', 'e']}) - value = config['l'][2].get() - self.assertEqual(value, 'e') + config = _root({"l": ["a", "b"]}, {"l": ["c", "d", "e"]}) + value = config["l"][2].get() + self.assertEqual(value, "e") def test_list_access_missing(self): - config = _root({'l': ['a', 'b']}, {'l': ['c', 'd', 'e']}) + config = _root({"l": ["a", "b"]}, {"l": ["c", "d", "e"]}) with self.assertRaises(confuse.NotFoundError): - config['l'][3].get() + config["l"][3].get() def test_access_dict_replaced(self): - config = _root({'foo': {'bar': 'baz'}}, {'foo': {'qux': 'fred'}}) - value = config['foo'].get() - self.assertEqual(value, {'bar': 'baz'}) + config = _root({"foo": {"bar": "baz"}}, {"foo": {"qux": "fred"}}) + value = config["foo"].get() + self.assertEqual(value, {"bar": "baz"}) def test_dict_keys_merged(self): - config = _root({'foo': {'bar': 'baz'}}, {'foo': {'qux': 'fred'}}) - keys = config['foo'].keys() - self.assertEqual(set(keys), set(['bar', 'qux'])) + config = _root({"foo": {"bar": "baz"}}, {"foo": {"qux": "fred"}}) + keys = config["foo"].keys() + self.assertEqual(set(keys), set(["bar", "qux"])) def test_dict_keys_replaced(self): - config = _root({'foo': {'bar': 'baz'}}, {'foo': {'bar': 'fred'}}) - keys = config['foo'].keys() - self.assertEqual(list(keys), ['bar']) + config = _root({"foo": {"bar": "baz"}}, {"foo": {"bar": "fred"}}) + keys = config["foo"].keys() + self.assertEqual(list(keys), ["bar"]) def test_dict_values_merged(self): - config = _root({'foo': {'bar': 'baz'}}, {'foo': {'qux': 'fred'}}) - values = [value.get() for value in config['foo'].values()] - self.assertEqual(set(values), set(['baz', 'fred'])) + config = _root({"foo": {"bar": "baz"}}, {"foo": {"qux": "fred"}}) + values = [value.get() for value in config["foo"].values()] + self.assertEqual(set(values), set(["baz", "fred"])) def test_dict_values_replaced(self): - config = _root({'foo': {'bar': 'baz'}}, {'foo': {'bar': 'fred'}}) - values = [value.get() for value in config['foo'].values()] - self.assertEqual(list(values), ['baz']) + config = _root({"foo": {"bar": "baz"}}, {"foo": {"bar": "fred"}}) + values = [value.get() for value in config["foo"].values()] + self.assertEqual(list(values), ["baz"]) def test_dict_items_merged(self): - config = _root({'foo': {'bar': 'baz'}}, {'foo': {'qux': 'fred'}}) - items = [(key, value.get()) for (key, value) in config['foo'].items()] - self.assertEqual(set(items), set([('bar', 'baz'), ('qux', 'fred')])) + config = _root({"foo": {"bar": "baz"}}, {"foo": {"qux": "fred"}}) + items = [(key, value.get()) for (key, value) in config["foo"].items()] + self.assertEqual(set(items), set([("bar", "baz"), ("qux", "fred")])) def test_dict_items_replaced(self): - config = _root({'foo': {'bar': 'baz'}}, {'foo': {'bar': 'fred'}}) - items = [(key, value.get()) for (key, value) in config['foo'].items()] - self.assertEqual(list(items), [('bar', 'baz')]) + config = _root({"foo": {"bar": "baz"}}, {"foo": {"bar": "fred"}}) + items = [(key, value.get()) for (key, value) in config["foo"].items()] + self.assertEqual(list(items), [("bar", "baz")]) def test_list_sequence_shadowed(self): - config = _root({'l': ['a', 'b']}, {'l': ['c', 'd', 'e']}) - items = [item.get() for item in config['l'].sequence()] - self.assertEqual(items, ['a', 'b']) + config = _root({"l": ["a", "b"]}, {"l": ["c", "d", "e"]}) + items = [item.get() for item in config["l"].sequence()] + self.assertEqual(items, ["a", "b"]) def test_list_sequence_shadowed_by_dict(self): - config = _root({'foo': {'bar': 'baz'}}, {'foo': ['qux', 'fred']}) + config = _root({"foo": {"bar": "baz"}}, {"foo": ["qux", "fred"]}) with self.assertRaises(confuse.ConfigTypeError): - list(config['foo'].sequence()) + list(config["foo"].sequence()) def test_dict_contents_concatenated(self): - config = _root({'foo': {'bar': 'baz'}}, {'foo': {'qux': 'fred'}}) - contents = config['foo'].all_contents() - self.assertEqual(set(contents), set(['bar', 'qux'])) + config = _root({"foo": {"bar": "baz"}}, {"foo": {"qux": "fred"}}) + contents = config["foo"].all_contents() + self.assertEqual(set(contents), set(["bar", "qux"])) def test_dict_contents_concatenated_not_replaced(self): - config = _root({'foo': {'bar': 'baz'}}, {'foo': {'bar': 'fred'}}) - contents = config['foo'].all_contents() - self.assertEqual(list(contents), ['bar', 'bar']) + config = _root({"foo": {"bar": "baz"}}, {"foo": {"bar": "fred"}}) + contents = config["foo"].all_contents() + self.assertEqual(list(contents), ["bar", "bar"]) def test_list_contents_concatenated(self): - config = _root({'foo': ['bar', 'baz']}, {'foo': ['qux', 'fred']}) - contents = config['foo'].all_contents() - self.assertEqual(list(contents), ['bar', 'baz', 'qux', 'fred']) + config = _root({"foo": ["bar", "baz"]}, {"foo": ["qux", "fred"]}) + contents = config["foo"].all_contents() + self.assertEqual(list(contents), ["bar", "baz", "qux", "fred"]) def test_int_contents_error(self): - config = _root({'foo': ['bar', 'baz']}, {'foo': 5}) + config = _root({"foo": ["bar", "baz"]}, {"foo": 5}) with self.assertRaises(confuse.ConfigTypeError): - list(config['foo'].all_contents()) + list(config["foo"].all_contents()) def test_list_and_dict_contents_concatenated(self): - config = _root({'foo': ['bar', 'baz']}, {'foo': {'qux': 'fred'}}) - contents = config['foo'].all_contents() - self.assertEqual(list(contents), ['bar', 'baz', 'qux']) + config = _root({"foo": ["bar", "baz"]}, {"foo": {"qux": "fred"}}) + contents = config["foo"].all_contents() + self.assertEqual(list(contents), ["bar", "baz", "qux"]) def test_add_source(self): - config = _root({'foo': 'bar'}) - config.add({'baz': 'qux'}) - self.assertEqual(config['foo'].get(), 'bar') - self.assertEqual(config['baz'].get(), 'qux') + config = _root({"foo": "bar"}) + config.add({"baz": "qux"}) + self.assertEqual(config["foo"].get(), "bar") + self.assertEqual(config["baz"].get(), "qux") class SetTest(unittest.TestCase): def test_set_missing_top_level_key(self): config = _root({}) - config['foo'] = 'bar' - self.assertEqual(config['foo'].get(), 'bar') + config["foo"] = "bar" + self.assertEqual(config["foo"].get(), "bar") def test_override_top_level_key(self): - config = _root({'foo': 'bar'}) - config['foo'] = 'baz' - self.assertEqual(config['foo'].get(), 'baz') + config = _root({"foo": "bar"}) + config["foo"] = "baz" + self.assertEqual(config["foo"].get(), "baz") def test_set_second_level_key(self): config = _root({}) - config['foo']['bar'] = 'baz' - self.assertEqual(config['foo']['bar'].get(), 'baz') + config["foo"]["bar"] = "baz" + self.assertEqual(config["foo"]["bar"].get(), "baz") def test_override_second_level_key(self): - config = _root({'foo': {'bar': 'qux'}}) - config['foo']['bar'] = 'baz' - self.assertEqual(config['foo']['bar'].get(), 'baz') + config = _root({"foo": {"bar": "qux"}}) + config["foo"]["bar"] = "baz" + self.assertEqual(config["foo"]["bar"].get(), "baz") def test_override_list_index(self): - config = _root({'foo': ['a', 'b', 'c']}) - config['foo'][1] = 'bar' - self.assertEqual(config['foo'][1].get(), 'bar') + config = _root({"foo": ["a", "b", "c"]}) + config["foo"][1] = "bar" + self.assertEqual(config["foo"][1].get(), "bar") diff --git a/test/test_yaml.py b/test/test_yaml.py index ccb6b30..1c4fa7d 100644 --- a/test/test_yaml.py +++ b/test/test_yaml.py @@ -1,6 +1,9 @@ -import confuse -import yaml import unittest + +import yaml + +import confuse + from . import TempDir @@ -12,98 +15,98 @@ class ParseTest(unittest.TestCase): def test_dict_parsed_as_ordereddict(self): v = load("a: b\nc: d") self.assertTrue(isinstance(v, confuse.OrderedDict)) - self.assertEqual(list(v), ['a', 'c']) + self.assertEqual(list(v), ["a", "c"]) def test_string_beginning_with_percent(self): v = load("foo: %bar") - self.assertEqual(v['foo'], '%bar') + self.assertEqual(v["foo"], "%bar") class FileParseTest(unittest.TestCase): def _parse_contents(self, contents): with TempDir() as temp: - path = temp.sub('test_config.yaml', contents) + path = temp.sub("test_config.yaml", contents) return confuse.load_yaml(path) def test_load_file(self): - v = self._parse_contents(b'foo: bar') - self.assertEqual(v['foo'], 'bar') + v = self._parse_contents(b"foo: bar") + self.assertEqual(v["foo"], "bar") def test_syntax_error(self): try: - self._parse_contents(b':') + self._parse_contents(b":") except confuse.ConfigError as exc: - self.assertTrue('test_config.yaml' in exc.name) + self.assertTrue("test_config.yaml" in exc.name) else: - self.fail('ConfigError not raised') + self.fail("ConfigError not raised") def test_reload_conf(self): with TempDir() as temp: - path = temp.sub('test_config.yaml', b'foo: bar') - config = confuse.Configuration('test', __name__) + path = temp.sub("test_config.yaml", b"foo: bar") + config = confuse.Configuration("test", __name__) config.set_file(filename=path) - self.assertEqual(config['foo'].get(), 'bar') - temp.sub('test_config.yaml', b'foo: bar2\ntest: hello world') + self.assertEqual(config["foo"].get(), "bar") + temp.sub("test_config.yaml", b"foo: bar2\ntest: hello world") config.reload() - self.assertEqual(config['foo'].get(), 'bar2') - self.assertEqual(config['test'].get(), 'hello world') + self.assertEqual(config["foo"].get(), "bar2") + self.assertEqual(config["test"].get(), "hello world") def test_tab_indentation_error(self): try: self._parse_contents(b"foo:\n\tbar: baz") except confuse.ConfigError as exc: - self.assertTrue('found tab' in exc.args[0]) + self.assertTrue("found tab" in exc.args[0]) else: - self.fail('ConfigError not raised') + self.fail("ConfigError not raised") class StringParseTest(unittest.TestCase): def test_load_string(self): - v = confuse.load_yaml_string('foo: bar', 'test') - self.assertEqual(v['foo'], 'bar') + v = confuse.load_yaml_string("foo: bar", "test") + self.assertEqual(v["foo"], "bar") def test_string_syntax_error(self): try: - confuse.load_yaml_string(':', 'test') + confuse.load_yaml_string(":", "test") except confuse.ConfigError as exc: - self.assertTrue('test' in exc.name) + self.assertTrue("test" in exc.name) else: - self.fail('ConfigError not raised') + self.fail("ConfigError not raised") def test_string_tab_indentation_error(self): try: - confuse.load_yaml_string('foo:\n\tbar: baz', 'test') + confuse.load_yaml_string("foo:\n\tbar: baz", "test") except confuse.ConfigError as exc: - self.assertTrue('found tab' in exc.args[0]) + self.assertTrue("found tab" in exc.args[0]) else: - self.fail('ConfigError not raised') + self.fail("ConfigError not raised") class ParseAsScalarTest(unittest.TestCase): def test_text_string(self): - v = confuse.yaml_util.parse_as_scalar('foo', confuse.Loader) - self.assertEqual(v, 'foo') + v = confuse.yaml_util.parse_as_scalar("foo", confuse.Loader) + self.assertEqual(v, "foo") def test_number_string_to_int(self): - v = confuse.yaml_util.parse_as_scalar('1', confuse.Loader) + v = confuse.yaml_util.parse_as_scalar("1", confuse.Loader) self.assertIsInstance(v, int) self.assertEqual(v, 1) def test_number_string_to_float(self): - v = confuse.yaml_util.parse_as_scalar('1.0', confuse.Loader) + v = confuse.yaml_util.parse_as_scalar("1.0", confuse.Loader) self.assertIsInstance(v, float) self.assertEqual(v, 1.0) def test_bool_string_to_bool(self): - v = confuse.yaml_util.parse_as_scalar('true', confuse.Loader) + v = confuse.yaml_util.parse_as_scalar("true", confuse.Loader) self.assertIs(v, True) def test_empty_string_to_none(self): - v = confuse.yaml_util.parse_as_scalar('', confuse.Loader) + v = confuse.yaml_util.parse_as_scalar("", confuse.Loader) self.assertIs(v, None) def test_null_string_to_none(self): - v = confuse.yaml_util.parse_as_scalar('null', confuse.Loader) + v = confuse.yaml_util.parse_as_scalar("null", confuse.Loader) self.assertIs(v, None) def test_dict_string_unchanged(self): @@ -111,17 +114,17 @@ def test_dict_string_unchanged(self): self.assertEqual(v, '{"foo": "bar"}') def test_dict_unchanged(self): - v = confuse.yaml_util.parse_as_scalar({'foo': 'bar'}, confuse.Loader) - self.assertEqual(v, {'foo': 'bar'}) + v = confuse.yaml_util.parse_as_scalar({"foo": "bar"}, confuse.Loader) + self.assertEqual(v, {"foo": "bar"}) def test_list_string_unchanged(self): v = confuse.yaml_util.parse_as_scalar('["foo", "bar"]', confuse.Loader) self.assertEqual(v, '["foo", "bar"]') def test_list_unchanged(self): - v = confuse.yaml_util.parse_as_scalar(['foo', 'bar'], confuse.Loader) - self.assertEqual(v, ['foo', 'bar']) + v = confuse.yaml_util.parse_as_scalar(["foo", "bar"], confuse.Loader) + self.assertEqual(v, ["foo", "bar"]) def test_invalid_yaml_string_unchanged(self): - v = confuse.yaml_util.parse_as_scalar('!', confuse.Loader) - self.assertEqual(v, '!') + v = confuse.yaml_util.parse_as_scalar("!", confuse.Loader) + self.assertEqual(v, "!") From 23bcec4bcabe3f1718b90e89d85cfa53d36a4445 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= Date: Sat, 27 Dec 2025 15:03:26 +0000 Subject: [PATCH 03/15] Fix linting issues --- confuse/__init__.py | 12 +- confuse/core.py | 57 ++++---- confuse/exceptions.py | 18 +-- confuse/sources.py | 10 +- confuse/templates.py | 79 ++++------- confuse/yaml_util.py | 12 +- docs/conf.py | 2 +- test/test_cli.py | 54 ++++---- test/test_dump.py | 32 ++--- test/test_env.py | 129 +++++++++--------- test/test_paths.py | 110 +++++++--------- test/test_utils.py | 40 +++--- test/test_valid.py | 286 ++++++++++++++++++++-------------------- test/test_validation.py | 44 ++++--- test/test_views.py | 104 ++++++++------- test/test_yaml.py | 67 ++++------ 16 files changed, 502 insertions(+), 554 deletions(-) diff --git a/confuse/__init__.py b/confuse/__init__.py index 9076be7..6c41fda 100644 --- a/confuse/__init__.py +++ b/confuse/__init__.py @@ -1,8 +1,8 @@ """Painless YAML configuration.""" -from .exceptions import * # NOQA -from .util import * # NOQA -from .yaml_util import * # NOQA -from .sources import * # NOQA -from .templates import * # NOQA -from .core import * # NOQA +from .core import * # noqa: F403 +from .exceptions import * # noqa: F403 +from .sources import * # noqa: F403 +from .templates import * # noqa: F403 +from .util import * # noqa: F403 +from .yaml_util import * # noqa: F403 diff --git a/confuse/core.py b/confuse/core.py index 52239d3..eef04be 100644 --- a/confuse/core.py +++ b/confuse/core.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of Confuse. # Copyright 2016, Adrian Sampson. # @@ -20,20 +19,19 @@ __all__ = [ "CONFIG_FILENAME", "DEFAULT_FILENAME", - "ROOT_NAME", "REDACTED_TOMBSTONE", + "ROOT_NAME", "ConfigView", - "RootView", - "Subview", "Configuration", "LazyConfig", + "RootView", + "Subview", ] import errno import os from collections import OrderedDict -from pathlib import Path -from typing import Any, Iterable, Sequence, TypeVar +from typing import TYPE_CHECKING, Any, TypeVar import yaml @@ -41,6 +39,10 @@ from .exceptions import ConfigError, ConfigTypeError, NotFoundError from .sources import ConfigSource, EnvSource, YamlSource +if TYPE_CHECKING: + from collections.abc import Iterable, Sequence + from pathlib import Path + CONFIG_FILENAME = "config.yaml" DEFAULT_FILENAME = "config_default.yaml" ROOT_NAME = "root" @@ -84,7 +86,7 @@ def first(self): try: return util.iter_first(pairs) except ValueError: - raise NotFoundError("{0} not found".format(self.name)) + raise NotFoundError(f"{self.name} not found") def exists(self): """Determine whether the view has a setting in any source.""" @@ -113,7 +115,7 @@ def root(self): raise NotImplementedError def __repr__(self): - return "<{}: {}>".format(self.__class__.__name__, self.name) + return f"<{self.__class__.__name__}: {self.name}>" def __iter__(self): """Iterate over the keys of a dictionary view or the *subviews* @@ -121,21 +123,18 @@ def __iter__(self): """ # Try iterating over the keys, if this is a dictionary view. try: - for key in self.keys(): - yield key + yield from self.keys() except ConfigTypeError: # Otherwise, try iterating over a list view. try: - for subview in self.sequence(): - yield subview + yield from self.sequence() except ConfigTypeError: item, _ = self.first() raise ConfigTypeError( - "{0} must be a dictionary or a list, not {1}".format( - self.name, type(item).__name__ - ) + f"{self.name} must be a dictionary or a list, not " + f"{type(item).__name__}" ) def __getitem__(self, key): @@ -200,7 +199,7 @@ def keys(self): cur_keys = dic.keys() except AttributeError: raise ConfigTypeError( - "{0} must be a dict, not {1}".format(self.name, type(dic).__name__) + f"{self.name} must be a dict, not {type(dic).__name__}" ) for key in cur_keys: @@ -238,9 +237,7 @@ def sequence(self): return if not isinstance(collection, (list, tuple)): raise ConfigTypeError( - "{0} must be a list, not {1}".format( - self.name, type(collection).__name__ - ) + f"{self.name} must be a list, not {type(collection).__name__}" ) # Yield all the indices in the sequence. @@ -259,12 +256,9 @@ def all_contents(self): it = iter(collection) except TypeError: raise ConfigTypeError( - "{0} must be an iterable, not {1}".format( - self.name, type(collection).__name__ - ) + f"{self.name} must be an iterable, not {type(collection).__name__}" ) - for value in it: - yield value + yield from it # Validation and conversion. @@ -434,7 +428,7 @@ def __init__(self, parent, key): if not isinstance(self.key, int): self.name += "." if isinstance(self.key, int): - self.name += "#{0}".format(self.key) + self.name += f"#{self.key}" elif isinstance(self.key, bytes): self.name += self.key.decode("utf-8") elif isinstance(self.key, str): @@ -455,9 +449,8 @@ def resolve(self): except TypeError: # Not subscriptable. raise ConfigTypeError( - "{0} must be a collection, not {1}".format( - self.parent.name, type(collection).__name__ - ) + f"{self.parent.name} must be a collection, not " + f"{type(collection).__name__}" ) yield value, source @@ -471,7 +464,7 @@ def root(self): return self.parent.root() def set_redaction(self, path, flag): - self.parent.set_redaction((self.key,) + path, flag) + self.parent.set_redaction((self.key, *path), flag) def get_redactions(self): return ( @@ -506,7 +499,7 @@ def __init__(self, appname, modname=None, read=True, loader=yaml_util.Loader): else: self._package_path = None - self._env_var = "{0}DIR".format(self.appname.upper()) + self._env_var = f"{self.appname.upper()}DIR" if read: self.read() @@ -567,7 +560,7 @@ def config_dir(self): appdir = os.environ[self._env_var] appdir = os.path.abspath(os.path.expanduser(appdir)) if os.path.isfile(appdir): - raise ConfigError("{0} must be a directory".format(self._env_var)) + raise ConfigError(f"{self._env_var} must be a directory") else: # Search platform-specific locations. If no config file is @@ -622,7 +615,7 @@ def set_env(self, prefix=None, sep="__"): :param sep: Separator within variable names to define nested keys. """ if prefix is None: - prefix = "{0}_".format(self.appname.upper()) + prefix = f"{self.appname.upper()}_" self.set(EnvSource(prefix, sep=sep, loader=self.loader)) def dump(self, full=True, redact=False): diff --git a/confuse/exceptions.py b/confuse/exceptions.py index 3b46696..1a4a87e 100644 --- a/confuse/exceptions.py +++ b/confuse/exceptions.py @@ -2,11 +2,11 @@ __all__ = [ "ConfigError", - "NotFoundError", - "ConfigValueError", - "ConfigTypeError", - "ConfigTemplateError", "ConfigReadError", + "ConfigTemplateError", + "ConfigTypeError", + "ConfigValueError", + "NotFoundError", ] YAML_TAB_PROBLEM = "found character '\\t' that cannot start any token" @@ -41,18 +41,18 @@ def __init__(self, name, reason=None): self.name = name self.reason = reason - message = "{0} could not be read".format(name) + message = f"{name} could not be read" if ( isinstance(reason, yaml.scanner.ScannerError) and reason.problem == YAML_TAB_PROBLEM ): # Special-case error message for tab indentation in YAML markup. - message += ": found tab character at line {0}, column {1}".format( - reason.problem_mark.line + 1, - reason.problem_mark.column + 1, + message += ( + f": found tab character at line {reason.problem_mark.line + 1}, " + f"column {reason.problem_mark.column + 1}" ) elif reason: # Generic error message uses exception's message. - message += ": {0}".format(reason) + message += f": {reason}" super().__init__(message) diff --git a/confuse/sources.py b/confuse/sources.py index a665aec..41d244b 100644 --- a/confuse/sources.py +++ b/confuse/sources.py @@ -33,11 +33,9 @@ def __init__(self, value, filename=None, default=False, base_for_paths=False): self.base_for_paths = base_for_paths if filename is not None else False def __repr__(self): - return "ConfigSource({0!r}, {1!r}, {2!r}, {3!r})".format( - super(), - self.filename, - self.default, - self.base_for_paths, + return ( + f"ConfigSource({super()!r}, {self.filename!r}, {self.default!r}, " + f"{self.base_for_paths!r})" ) @classmethod @@ -143,7 +141,7 @@ def load(self): # appropriate object (ie, '{foo: bar}' to {'foo': 'bar'}). # Will raise a ConfigReadError if YAML parsing fails. value = yaml_util.load_yaml_string( - value, "env variable " + var, loader=self.loader + value, f"env variable {var}", loader=self.loader ) else: # Parse the value as a YAML scalar so that values are type diff --git a/confuse/templates.py b/confuse/templates.py index 7ff1503..5094dfe 100644 --- a/confuse/templates.py +++ b/confuse/templates.py @@ -58,7 +58,7 @@ def get_default_value(self, key_name="default"): """ if not hasattr(self, "default") or self.default is REQUIRED: # The value is required. A missing value is an error. - raise exceptions.NotFoundError("{} not found".format(key_name)) + raise exceptions.NotFoundError(f"{key_name} not found") # The value is not required. return self.default @@ -83,10 +83,10 @@ def fail(self, message, view, type_error=False): exc_class = ( exceptions.ConfigTypeError if type_error else exceptions.ConfigValueError ) - raise exc_class("{0}: {1}".format(view.name, message)) + raise exc_class(f"{view.name}: {message}") def __repr__(self): - return "{0}({1})".format( + return "{}({})".format( type(self).__name__, "" if self.default is REQUIRED else repr(self.default), ) @@ -113,9 +113,7 @@ def convert(self, value, view): if isinstance(value, (int, float)): return value else: - self.fail( - "must be numeric, not {0}".format(type(value).__name__), view, True - ) + self.fail(f"must be numeric, not {type(value).__name__}", view, True) class MappingTemplate(Template): @@ -143,7 +141,7 @@ def value(self, view, template=None): return out def __repr__(self): - return "MappingTemplate({0})".format(repr(self.subtemplates)) + return f"MappingTemplate({self.subtemplates!r})" class Sequence(Template): @@ -165,7 +163,7 @@ def value(self, view, template=None): return out def __repr__(self): - return "Sequence({0})".format(repr(self.subtemplate)) + return f"Sequence({self.subtemplate!r})" class MappingValues(Template): @@ -193,7 +191,7 @@ def value(self, view, template=None): return out def __repr__(self): - return "MappingValues({0})".format(repr(self.subtemplate)) + return f"MappingValues({self.subtemplate!r})" class String(Template): @@ -218,7 +216,7 @@ def __repr__(self): if self.pattern is not None: args.append("pattern=" + repr(self.pattern)) - return "String({0})".format(", ".join(args)) + return f"String({', '.join(args)})" def convert(self, value, view): """Check that the value is a string and matches the pattern.""" @@ -226,7 +224,7 @@ def convert(self, value, view): self.fail("must be a string", view, True) if self.pattern and not self.regex.match(value): - self.fail("must match the pattern {0}".format(self.pattern), view) + self.fail(f"must match the pattern {self.pattern}", view) if self.expand_vars: return os.path.expandvars(value) @@ -263,15 +261,14 @@ def convert(self, value, view): return self.choices(value) except ValueError: self.fail( - "must be one of {0!r}, not {1!r}".format( - [c.value for c in self.choices], value - ), + f"must be one of {[c.value for c in self.choices]!r}, not " + f"{value!r}", view, ) if value not in self.choices: self.fail( - "must be one of {0!r}, not {1!r}".format(list(self.choices), value), + f"must be one of {list(self.choices)!r}, not {value!r}", view, ) @@ -281,7 +278,7 @@ def convert(self, value, view): return value def __repr__(self): - return "Choice({0!r})".format(self.choices) + return f"Choice({self.choices!r})" class OneOf(Template): @@ -300,7 +297,7 @@ def __repr__(self): if self.default is not REQUIRED: args.append(repr(self.default)) - return "OneOf({0})".format(", ".join(args)) + return f"OneOf({', '.join(args)})" def value(self, view, template): self.template = template @@ -324,9 +321,7 @@ def convert(self, value, view): except ValueError as exc: raise exceptions.ConfigTemplateError(exc) - self.fail( - "must be one of {0}, not {1}".format(repr(self.allowed), repr(value)), view - ) + self.fail(f"must be one of {self.allowed!r}, not {value!r}", view) class StrSeq(Template): @@ -409,9 +404,7 @@ def _convert_value(self, x, view): else: # Is this even possible? -> Likely, if some !directive cause # YAML to parse this to some custom type. - self.fail( - "must be a single string, mapping, or a list" + str(x), view, True - ) + self.fail(f"must be a single string, mapping, or a list{x}", view, True) return (super()._convert_value(k, view), super()._convert_value(v, view)) @@ -473,7 +466,7 @@ def __repr__(self): if self.in_source_dir: args.append("in_source_dir=True") - return "Filename({0})".format(", ".join(args)) + return f"Filename({', '.join(args)})" def resolve_relative_to(self, view, template): if not isinstance(template, (abc.Mapping, MappingTemplate)): @@ -483,16 +476,12 @@ def resolve_relative_to(self, view, template): ) elif self.relative_to == view.key: - raise exceptions.ConfigTemplateError( - "{0} is relative to itself".format(view.name) - ) + raise exceptions.ConfigTemplateError(f"{view.name} is relative to itself") elif self.relative_to not in view.parent.keys(): # self.relative_to is not in the config self.fail( - ('needs sibling value "{0}" to expand relative path').format( - self.relative_to - ), + (f'needs sibling value "{self.relative_to}" to expand relative path'), view, ) @@ -513,16 +502,12 @@ def resolve_relative_to(self, view, template): if next_relative in template.subtemplates: # we encountered this config key previously raise exceptions.ConfigTemplateError( - ("{0} and {1} are recursively relative").format( - view.name, self.relative_to - ) + f"{view.name} and {self.relative_to} are recursively relative" ) else: raise exceptions.ConfigTemplateError( - ( - "missing template for {0}, needed to expand {1}'s" - "relative path" - ).format(self.relative_to, view.name) + f"missing template for {self.relative_to}, needed to expand " + f"{view.name}'s relative path" ) next_template.subtemplates[next_relative] = rel_to_template @@ -537,9 +522,7 @@ def value(self, view, template=None): return self.get_default_value(view.name) if not isinstance(path, str): - self.fail( - "must be a filename, not {0}".format(type(path).__name__), view, True - ) + self.fail(f"must be a filename, not {type(path).__name__}", view, True) path = os.path.expanduser(str(path)) if not os.path.isabs(path): @@ -611,7 +594,7 @@ def value(self, view, template=None): # Value is missing but not required return self.default # Value must be present even though it can be null. Raise an error. - raise exceptions.NotFoundError("{} not found".format(view.name)) + raise exceptions.NotFoundError(f"{view.name} not found") if value is None: # None (ie, null) is always a valid value @@ -619,10 +602,9 @@ def value(self, view, template=None): return self.subtemplate.value(view, self) def __repr__(self): - return "Optional({0}, {1}, allow_missing={2})".format( - repr(self.subtemplate), - repr(self.default), - self.allow_missing, + return ( + f"Optional({self.subtemplate!r}, {self.default!r}, " + f"allow_missing={self.allow_missing})" ) @@ -641,10 +623,7 @@ def __init__(self, typ, default=REQUIRED): def convert(self, value, view): if not isinstance(value, self.typ): self.fail( - "must be a {0}, not {1}".format( - self.typ.__name__, - type(value).__name__, - ), + f"must be a {self.typ.__name__}, not {type(value).__name__}", view, True, ) @@ -706,4 +685,4 @@ def as_template(value): elif isinstance(value, type): return TypeTemplate(value) else: - raise ValueError("cannot convert to template: {0!r}".format(value)) + raise ValueError(f"cannot convert to template: {value!r}") diff --git a/confuse/yaml_util.py b/confuse/yaml_util.py index cda6189..3796a5a 100644 --- a/confuse/yaml_util.py +++ b/confuse/yaml_util.py @@ -35,7 +35,7 @@ def construct_mapping(self, node, deep=False): raise yaml.constructor.ConstructorError( None, None, - "expected a mapping node, but found %s" % node.id, + f"expected a mapping node, but found {node.id}", node.start_mark, ) @@ -48,7 +48,7 @@ def construct_mapping(self, node, deep=False): raise yaml.constructor.ConstructorError( "while constructing a mapping", node.start_mark, - "found unacceptable key (%s)" % exc, + f"found unacceptable key ({exc})", key_node.start_mark, ) value = self.construct_object(value_node, deep=deep) @@ -84,7 +84,7 @@ def load_yaml(filename, loader=Loader): try: with open(filename, "rb") as f: return yaml.load(f, Loader=loader) - except (IOError, yaml.error.YAMLError) as exc: + except (OSError, yaml.error.YAMLError) as exc: raise ConfigReadError(filename, exc) @@ -205,14 +205,14 @@ def restore_yaml_comments(data, default_data): if not line: comment = "\n" elif line.startswith("#"): - comment = "{0}\n".format(line) + comment = f"{line}\n" else: continue while True: line = next(default_lines) if line and not line.startswith("#"): break - comment += "{0}\n".format(line) + comment += f"{line}\n" key = line.split(":")[0].strip() comment_map[key] = comment out_lines = iter(data.splitlines()) @@ -221,5 +221,5 @@ def restore_yaml_comments(data, default_data): key = line.split(":")[0].strip() if key in comment_map: out_data += comment_map[key] - out_data += "{0}\n".format(line) + out_data += f"{line}\n" return out_data diff --git a/docs/conf.py b/docs/conf.py index 5ecca8d..a6a2e85 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -25,7 +25,7 @@ master_doc = "index" project = "Confuse" -copyright = "2012-{}, Adrian Sampson & contributors".format(dt.date.today().year) +copyright = f"2012-{dt.date.today().year}, Adrian Sampson & contributors" exclude_patterns = ["_build"] diff --git a/test/test_cli.py b/test/test_cli.py index 76a2879..81be841 100644 --- a/test/test_cli.py +++ b/test/test_cli.py @@ -3,6 +3,8 @@ import unittest from argparse import Namespace +import pytest + import confuse @@ -18,17 +20,17 @@ def _parse(self, args, **kwargs): def test_text_argument_parsed(self): self.parser.add_argument("--foo", metavar="BAR") self._parse("--foo bar") - self.assertEqual(self.config["foo"].get(), "bar") + assert self.config["foo"].get() == "bar" def test_boolean_argument_parsed(self): self.parser.add_argument("--foo", action="store_true") self._parse("--foo") - self.assertEqual(self.config["foo"].get(), True) + assert self.config["foo"].get() def test_missing_optional_argument_not_included(self): self.parser.add_argument("--foo", metavar="BAR") self._parse("") - with self.assertRaises(confuse.NotFoundError): + with pytest.raises(confuse.NotFoundError): self.config["foo"].get() def test_argument_overrides_default(self): @@ -36,36 +38,36 @@ def test_argument_overrides_default(self): self.parser.add_argument("--foo", metavar="BAR") self._parse("--foo bar") - self.assertEqual(self.config["foo"].get(), "bar") + assert self.config["foo"].get() == "bar" def test_nested_destination_single(self): self.parser.add_argument("--one", dest="one.foo") self.parser.add_argument("--two", dest="one.two.foo") self._parse("--two TWO", dots=True) - self.assertEqual(self.config["one"]["two"]["foo"].get(), "TWO") + assert self.config["one"]["two"]["foo"].get() == "TWO" def test_nested_destination_nested(self): self.parser.add_argument("--one", dest="one.foo") self.parser.add_argument("--two", dest="one.two.foo") self._parse("--two TWO --one ONE", dots=True) - self.assertEqual(self.config["one"]["foo"].get(), "ONE") - self.assertEqual(self.config["one"]["two"]["foo"].get(), "TWO") + assert self.config["one"]["foo"].get() == "ONE" + assert self.config["one"]["two"]["foo"].get() == "TWO" def test_nested_destination_nested_rev(self): self.parser.add_argument("--one", dest="one.foo") self.parser.add_argument("--two", dest="one.two.foo") # Reverse to ensure order doesn't matter self._parse("--one ONE --two TWO", dots=True) - self.assertEqual(self.config["one"]["foo"].get(), "ONE") - self.assertEqual(self.config["one"]["two"]["foo"].get(), "TWO") + assert self.config["one"]["foo"].get() == "ONE" + assert self.config["one"]["two"]["foo"].get() == "TWO" def test_nested_destination_clobber(self): self.parser.add_argument("--one", dest="one.two") self.parser.add_argument("--two", dest="one.two.foo") self._parse("--two TWO --one ONE", dots=True) # Clobbered - self.assertEqual(self.config["one"]["two"].get(), {"foo": "TWO"}) - self.assertEqual(self.config["one"]["two"]["foo"].get(), "TWO") + assert self.config["one"]["two"].get() == {"foo": "TWO"} + assert self.config["one"]["two"]["foo"].get() == "TWO" def test_nested_destination_clobber_rev(self): # Reversed order @@ -73,8 +75,8 @@ def test_nested_destination_clobber_rev(self): self.parser.add_argument("--one", dest="one.two") self._parse("--one ONE --two TWO", dots=True) # Clobbered just the same - self.assertEqual(self.config["one"]["two"].get(), {"foo": "TWO"}) - self.assertEqual(self.config["one"]["two"]["foo"].get(), "TWO") + assert self.config["one"]["two"].get() == {"foo": "TWO"} + assert self.config["one"]["two"]["foo"].get() == "TWO" class OptparseTest(unittest.TestCase): @@ -89,17 +91,17 @@ def _parse(self, args, **kwargs): def test_text_argument_parsed(self): self.parser.add_option("--foo", metavar="BAR") self._parse("--foo bar") - self.assertEqual(self.config["foo"].get(), "bar") + assert self.config["foo"].get() == "bar" def test_boolean_argument_parsed(self): self.parser.add_option("--foo", action="store_true") self._parse("--foo") - self.assertEqual(self.config["foo"].get(), True) + assert self.config["foo"].get() def test_missing_optional_argument_not_included(self): self.parser.add_option("--foo", metavar="BAR") self._parse("") - with self.assertRaises(confuse.NotFoundError): + with pytest.raises(confuse.NotFoundError): self.config["foo"].get() def test_argument_overrides_default(self): @@ -107,28 +109,28 @@ def test_argument_overrides_default(self): self.parser.add_option("--foo", metavar="BAR") self._parse("--foo bar") - self.assertEqual(self.config["foo"].get(), "bar") + assert self.config["foo"].get() == "bar" def test_nested_destination_single(self): self.parser.add_option("--one", dest="one.foo") self.parser.add_option("--two", dest="one.two.foo") self._parse("--two TWO", dots=True) - self.assertEqual(self.config["one"]["two"]["foo"].get(), "TWO") + assert self.config["one"]["two"]["foo"].get() == "TWO" def test_nested_destination_nested(self): self.parser.add_option("--one", dest="one.foo") self.parser.add_option("--two", dest="one.two.foo") self._parse("--two TWO --one ONE", dots=True) - self.assertEqual(self.config["one"]["foo"].get(), "ONE") - self.assertEqual(self.config["one"]["two"]["foo"].get(), "TWO") + assert self.config["one"]["foo"].get() == "ONE" + assert self.config["one"]["two"]["foo"].get() == "TWO" def test_nested_destination_nested_rev(self): self.parser.add_option("--one", dest="one.foo") self.parser.add_option("--two", dest="one.two.foo") # Reverse to ensure order doesn't matter self._parse("--one ONE --two TWO", dots=True) - self.assertEqual(self.config["one"]["foo"].get(), "ONE") - self.assertEqual(self.config["one"]["two"]["foo"].get(), "TWO") + assert self.config["one"]["foo"].get() == "ONE" + assert self.config["one"]["two"]["foo"].get() == "TWO" class GenericNamespaceTest(unittest.TestCase): @@ -137,14 +139,14 @@ def setUp(self): def test_value_added_to_root(self): self.config.set_args(Namespace(foo="bar")) - self.assertEqual(self.config["foo"].get(), "bar") + assert self.config["foo"].get() == "bar" def test_value_added_to_subview(self): self.config["baz"].set_args(Namespace(foo="bar")) - self.assertEqual(self.config["baz"]["foo"].get(), "bar") + assert self.config["baz"]["foo"].get() == "bar" def test_nested_namespace(self): args = Namespace(first="Hello", nested=Namespace(second="World")) self.config.set_args(args, dots=True) - self.assertEqual(self.config["first"].get(), "Hello") - self.assertEqual(self.config["nested"]["second"].get(), "World") + assert self.config["first"].get() == "Hello" + assert self.config["nested"]["second"].get() == "World" diff --git a/test/test_dump.py b/test/test_dump.py index 75d47e1..875b472 100644 --- a/test/test_dump.py +++ b/test/test_dump.py @@ -11,25 +11,25 @@ def test_dump_null(self): config = confuse.Configuration("myapp", read=False) config.add({"foo": None}) yaml = config.dump().strip() - self.assertEqual(yaml, "foo:") + assert yaml == "foo:" def test_dump_true(self): config = confuse.Configuration("myapp", read=False) config.add({"foo": True}) yaml = config.dump().strip() - self.assertEqual(yaml, "foo: yes") + assert yaml == "foo: yes" def test_dump_false(self): config = confuse.Configuration("myapp", read=False) config.add({"foo": False}) yaml = config.dump().strip() - self.assertEqual(yaml, "foo: no") + assert yaml == "foo: no" def test_dump_short_list(self): config = confuse.Configuration("myapp", read=False) config.add({"foo": ["bar", "baz"]}) yaml = config.dump().strip() - self.assertEqual(yaml, "foo: [bar, baz]") + assert yaml == "foo: [bar, baz]" def test_dump_ordered_dict(self): odict = confuse.OrderedDict() @@ -40,14 +40,14 @@ def test_dump_ordered_dict(self): config = confuse.Configuration("myapp", read=False) config.add({"key": odict}) yaml = config.dump().strip() - self.assertEqual( - yaml, - textwrap.dedent(""" + assert ( + yaml + == textwrap.dedent(""" key: foo: bar bar: baz baz: qux - """).strip(), + """).strip() ) def test_dump_sans_defaults(self): @@ -57,44 +57,44 @@ def test_dump_sans_defaults(self): config.add({"baz": "qux"}) yaml = config.dump().strip() - self.assertEqual(yaml, "foo: bar\nbaz: qux") + assert yaml == "foo: bar\nbaz: qux" yaml = config.dump(full=False).strip() - self.assertEqual(yaml, "baz: qux") + assert yaml == "baz: qux" class RedactTest(unittest.TestCase): def test_no_redaction(self): config = _root({"foo": "bar"}) data = config.flatten(redact=True) - self.assertEqual(data, {"foo": "bar"}) + assert data == {"foo": "bar"} def test_redact_key(self): config = _root({"foo": "bar"}) config["foo"].redact = True data = config.flatten(redact=True) - self.assertEqual(data, {"foo": "REDACTED"}) + assert data == {"foo": "REDACTED"} def test_unredact(self): config = _root({"foo": "bar"}) config["foo"].redact = True config["foo"].redact = False data = config.flatten(redact=True) - self.assertEqual(data, {"foo": "bar"}) + assert data == {"foo": "bar"} def test_dump_redacted(self): config = confuse.Configuration("myapp", read=False) config.add({"foo": "bar"}) config["foo"].redact = True yaml = config.dump(redact=True).strip() - self.assertEqual(yaml, "foo: REDACTED") + assert yaml == "foo: REDACTED" def test_dump_unredacted(self): config = confuse.Configuration("myapp", read=False) config.add({"foo": "bar"}) config["foo"].redact = True yaml = config.dump(redact=False).strip() - self.assertEqual(yaml, "foo: bar") + assert yaml == "foo: bar" def test_dump_redacted_sans_defaults(self): config = confuse.Configuration("myapp", read=False) @@ -104,4 +104,4 @@ def test_dump_redacted_sans_defaults(self): config["baz"].redact = True yaml = config.dump(redact=True, full=False).strip() - self.assertEqual(yaml, "baz: REDACTED") + assert yaml == "baz: REDACTED" diff --git a/test/test_env.py b/test/test_env.py index 7fef6d3..b3a9b7c 100644 --- a/test/test_env.py +++ b/test/test_env.py @@ -1,6 +1,8 @@ import os import unittest +import pytest + import confuse from . import _root @@ -19,7 +21,7 @@ def test_prefix(self): os.environ["TEST_FOO"] = "a" os.environ["BAR"] = "b" config = _root(confuse.EnvSource("TEST_")) - self.assertEqual(config.get(), {"foo": "a"}) + assert config.get() == {"foo": "a"} def test_number_type_conversion(self): os.environ["TEST_FOO"] = "1" @@ -27,68 +29,68 @@ def test_number_type_conversion(self): config = _root(confuse.EnvSource("TEST_")) foo = config["foo"].get() bar = config["bar"].get() - self.assertIsInstance(foo, int) - self.assertEqual(foo, 1) - self.assertIsInstance(bar, float) - self.assertEqual(bar, 2.0) + assert isinstance(foo, int) + assert foo == 1 + assert isinstance(bar, float) + assert bar == 2.0 def test_bool_type_conversion(self): os.environ["TEST_FOO"] = "true" os.environ["TEST_BAR"] = "FALSE" config = _root(confuse.EnvSource("TEST_")) - self.assertIs(config["foo"].get(), True) - self.assertIs(config["bar"].get(), False) + assert config["foo"].get() is True + assert config["bar"].get() is False def test_null_type_conversion(self): os.environ["TEST_FOO"] = "null" os.environ["TEST_BAR"] = "" config = _root(confuse.EnvSource("TEST_")) - self.assertIs(config["foo"].get(), None) - self.assertIs(config["bar"].get(), None) + assert config["foo"].get() is None + assert config["bar"].get() is None def test_unset_lower_config(self): os.environ["TEST_FOO"] = "null" config = _root({"foo": "bar"}) - self.assertEqual(config["foo"].get(), "bar") + assert config["foo"].get() == "bar" config.set(confuse.EnvSource("TEST_")) - self.assertIs(config["foo"].get(), None) + assert config["foo"].get() is None def test_sep_default(self): os.environ["TEST_FOO__BAR"] = "a" os.environ["TEST_FOO_BAZ"] = "b" config = _root(confuse.EnvSource("TEST_")) - self.assertEqual(config["foo"]["bar"].get(), "a") - self.assertEqual(config["foo_baz"].get(), "b") + assert config["foo"]["bar"].get() == "a" + assert config["foo_baz"].get() == "b" def test_sep_single_underscore_adjacent_seperators(self): os.environ["TEST_FOO__BAR"] = "a" os.environ["TEST_FOO_BAZ"] = "b" config = _root(confuse.EnvSource("TEST_", sep="_")) - self.assertEqual(config["foo"][""]["bar"].get(), "a") - self.assertEqual(config["foo"]["baz"].get(), "b") + assert config["foo"][""]["bar"].get() == "a" + assert config["foo"]["baz"].get() == "b" def test_nested(self): os.environ["TEST_FOO__BAR"] = "a" os.environ["TEST_FOO__BAZ__QUX"] = "b" config = _root(confuse.EnvSource("TEST_")) - self.assertEqual(config["foo"]["bar"].get(), "a") - self.assertEqual(config["foo"]["baz"]["qux"].get(), "b") + assert config["foo"]["bar"].get() == "a" + assert config["foo"]["baz"]["qux"].get() == "b" def test_nested_rev(self): # Reverse to ensure order doesn't matter os.environ["TEST_FOO__BAZ__QUX"] = "b" os.environ["TEST_FOO__BAR"] = "a" config = _root(confuse.EnvSource("TEST_")) - self.assertEqual(config["foo"]["bar"].get(), "a") - self.assertEqual(config["foo"]["baz"]["qux"].get(), "b") + assert config["foo"]["bar"].get() == "a" + assert config["foo"]["baz"]["qux"].get() == "b" def test_nested_clobber(self): os.environ["TEST_FOO__BAR"] = "a" os.environ["TEST_FOO__BAR__BAZ"] = "b" config = _root(confuse.EnvSource("TEST_")) # Clobbered - self.assertEqual(config["foo"]["bar"].get(), {"baz": "b"}) - self.assertEqual(config["foo"]["bar"]["baz"].get(), "b") + assert config["foo"]["bar"].get() == {"baz": "b"} + assert config["foo"]["bar"]["baz"].get() == "b" def test_nested_clobber_rev(self): # Reverse to ensure order doesn't matter @@ -96,35 +98,35 @@ def test_nested_clobber_rev(self): os.environ["TEST_FOO__BAR"] = "a" config = _root(confuse.EnvSource("TEST_")) # Clobbered - self.assertEqual(config["foo"]["bar"].get(), {"baz": "b"}) - self.assertEqual(config["foo"]["bar"]["baz"].get(), "b") + assert config["foo"]["bar"].get() == {"baz": "b"} + assert config["foo"]["bar"]["baz"].get() == "b" def test_lower_applied_after_prefix_match(self): os.environ["TEST_FOO"] = "a" config = _root(confuse.EnvSource("test_", lower=True)) - self.assertEqual(config.get(), {}) + assert config.get() == {} def test_lower_already_lowercase(self): os.environ["TEST_foo"] = "a" config = _root(confuse.EnvSource("TEST_", lower=True)) - self.assertEqual(config.get(), {"foo": "a"}) + assert config.get() == {"foo": "a"} def test_lower_does_not_alter_value(self): os.environ["TEST_FOO"] = "UPPER" config = _root(confuse.EnvSource("TEST_", lower=True)) - self.assertEqual(config.get(), {"foo": "UPPER"}) + assert config.get() == {"foo": "UPPER"} def test_lower_false(self): os.environ["TEST_FOO"] = "a" config = _root(confuse.EnvSource("TEST_", lower=False)) - self.assertEqual(config.get(), {"FOO": "a"}) + assert config.get() == {"FOO": "a"} def test_handle_lists_good_list(self): os.environ["TEST_FOO__0"] = "a" os.environ["TEST_FOO__1"] = "b" os.environ["TEST_FOO__2"] = "c" config = _root(confuse.EnvSource("TEST_", handle_lists=True)) - self.assertEqual(config["foo"].get(), ["a", "b", "c"]) + assert config["foo"].get() == ["a", "b", "c"] def test_handle_lists_good_list_rev(self): # Reverse to ensure order doesn't matter @@ -132,106 +134,102 @@ def test_handle_lists_good_list_rev(self): os.environ["TEST_FOO__1"] = "b" os.environ["TEST_FOO__0"] = "a" config = _root(confuse.EnvSource("TEST_", handle_lists=True)) - self.assertEqual(config["foo"].get(), ["a", "b", "c"]) + assert config["foo"].get() == ["a", "b", "c"] def test_handle_lists_nested_lists(self): os.environ["TEST_FOO__0__0"] = "a" os.environ["TEST_FOO__0__1"] = "b" os.environ["TEST_FOO__1__0"] = "c" config = _root(confuse.EnvSource("TEST_", handle_lists=True)) - self.assertEqual(config["foo"].get(), [["a", "b"], ["c"]]) + assert config["foo"].get() == [["a", "b"], ["c"]] def test_handle_lists_bad_list_missing_index(self): os.environ["TEST_FOO__0"] = "a" os.environ["TEST_FOO__2"] = "b" os.environ["TEST_FOO__3"] = "c" config = _root(confuse.EnvSource("TEST_", handle_lists=True)) - self.assertEqual(config["foo"].get(), {"0": "a", "2": "b", "3": "c"}) + assert config["foo"].get() == {"0": "a", "2": "b", "3": "c"} def test_handle_lists_bad_list_non_zero_start(self): os.environ["TEST_FOO__1"] = "a" os.environ["TEST_FOO__2"] = "b" os.environ["TEST_FOO__3"] = "c" config = _root(confuse.EnvSource("TEST_", handle_lists=True)) - self.assertEqual(config["foo"].get(), {"1": "a", "2": "b", "3": "c"}) + assert config["foo"].get() == {"1": "a", "2": "b", "3": "c"} def test_handle_lists_bad_list_non_numeric(self): os.environ["TEST_FOO__0"] = "a" os.environ["TEST_FOO__ONE"] = "b" os.environ["TEST_FOO__2"] = "c" config = _root(confuse.EnvSource("TEST_", handle_lists=True)) - self.assertEqual(config["foo"].get(), {"0": "a", "one": "b", "2": "c"}) + assert config["foo"].get() == {"0": "a", "one": "b", "2": "c"} def test_handle_lists_top_level_always_dict(self): os.environ["TEST_0"] = "a" os.environ["TEST_1"] = "b" os.environ["TEST_2"] = "c" config = _root(confuse.EnvSource("TEST_", handle_lists=True)) - self.assertEqual(config.get(), {"0": "a", "1": "b", "2": "c"}) + assert config.get() == {"0": "a", "1": "b", "2": "c"} def test_handle_lists_not_a_list(self): os.environ["TEST_FOO__BAR"] = "a" os.environ["TEST_FOO__BAZ"] = "b" config = _root(confuse.EnvSource("TEST_", handle_lists=True)) - self.assertEqual(config["foo"].get(), {"bar": "a", "baz": "b"}) + assert config["foo"].get() == {"bar": "a", "baz": "b"} def test_parse_yaml_docs_scalar(self): os.environ["TEST_FOO"] = "a" config = _root(confuse.EnvSource("TEST_", parse_yaml_docs=True)) - self.assertEqual(config["foo"].get(), "a") + assert config["foo"].get() == "a" def test_parse_yaml_docs_list(self): os.environ["TEST_FOO"] = "[a, b]" config = _root(confuse.EnvSource("TEST_", parse_yaml_docs=True)) - self.assertEqual(config["foo"].get(), ["a", "b"]) + assert config["foo"].get() == ["a", "b"] def test_parse_yaml_docs_dict(self): os.environ["TEST_FOO"] = "{bar: a, baz: b}" config = _root(confuse.EnvSource("TEST_", parse_yaml_docs=True)) - self.assertEqual(config["foo"].get(), {"bar": "a", "baz": "b"}) + assert config["foo"].get() == {"bar": "a", "baz": "b"} def test_parse_yaml_docs_nested(self): os.environ["TEST_FOO"] = "{bar: [a, b], baz: {qux: c}}" config = _root(confuse.EnvSource("TEST_", parse_yaml_docs=True)) - self.assertEqual(config["foo"]["bar"].get(), ["a", "b"]) - self.assertEqual(config["foo"]["baz"].get(), {"qux": "c"}) + assert config["foo"]["bar"].get() == ["a", "b"] + assert config["foo"]["baz"].get() == {"qux": "c"} def test_parse_yaml_docs_number_conversion(self): os.environ["TEST_FOO"] = "{bar: 1, baz: 2.0}" config = _root(confuse.EnvSource("TEST_", parse_yaml_docs=True)) bar = config["foo"]["bar"].get() baz = config["foo"]["baz"].get() - self.assertIsInstance(bar, int) - self.assertEqual(bar, 1) - self.assertIsInstance(baz, float) - self.assertEqual(baz, 2.0) + assert isinstance(bar, int) + assert bar == 1 + assert isinstance(baz, float) + assert baz == 2.0 def test_parse_yaml_docs_bool_conversion(self): os.environ["TEST_FOO"] = "{bar: true, baz: FALSE}" config = _root(confuse.EnvSource("TEST_", parse_yaml_docs=True)) - self.assertIs(config["foo"]["bar"].get(), True) - self.assertIs(config["foo"]["baz"].get(), False) + assert config["foo"]["bar"].get() is True + assert config["foo"]["baz"].get() is False def test_parse_yaml_docs_null_conversion(self): os.environ["TEST_FOO"] = "{bar: null, baz: }" config = _root(confuse.EnvSource("TEST_", parse_yaml_docs=True)) - self.assertIs(config["foo"]["bar"].get(), None) - self.assertIs(config["foo"]["baz"].get(), None) + assert config["foo"]["bar"].get() is None + assert config["foo"]["baz"].get() is None def test_parse_yaml_docs_syntax_error(self): os.environ["TEST_FOO"] = "{:}" - try: + with pytest.raises(confuse.ConfigError, match="TEST_FOO"): _root(confuse.EnvSource("TEST_", parse_yaml_docs=True)) - except confuse.ConfigError as exc: - self.assertTrue("TEST_FOO" in exc.name) - else: - self.fail("ConfigError not raised") def test_parse_yaml_docs_false(self): os.environ["TEST_FOO"] = "{bar: a, baz: b}" config = _root(confuse.EnvSource("TEST_", parse_yaml_docs=False)) - self.assertEqual(config["foo"].get(), "{bar: a, baz: b}") - with self.assertRaises(confuse.ConfigError): + assert config["foo"].get() == "{bar: a, baz: b}" + with pytest.raises(confuse.ConfigError): config["foo"]["bar"].get() @@ -250,21 +248,24 @@ def tearDown(self): def test_defaults(self): self.config.set_env() - self.assertEqual( - self.config.get(), - {"foo": "a", "bar": {"nested": "b"}, "baz_sep_nested": "c"}, - ) + assert self.config.get() == { + "foo": "a", + "bar": {"nested": "b"}, + "baz_sep_nested": "c", + } def test_with_prefix(self): self.config.set_env(prefix="MYAPP_") - self.assertEqual(self.config.get(), {"qux_sep_nested": "d"}) + assert self.config.get() == {"qux_sep_nested": "d"} def test_with_sep(self): self.config.set_env(sep="_sep_") - self.assertEqual( - self.config.get(), {"foo": "a", "bar__nested": "b", "baz": {"nested": "c"}} - ) + assert self.config.get() == { + "foo": "a", + "bar__nested": "b", + "baz": {"nested": "c"}, + } def test_with_prefix_and_sep(self): self.config.set_env(prefix="MYAPP_", sep="_sep_") - self.assertEqual(self.config.get(), {"qux": {"nested": "d"}}) + assert self.config.get() == {"qux": {"nested": "d"}} diff --git a/test/test_paths.py b/test/test_paths.py index 9d1d30b..15e5ce1 100644 --- a/test/test_paths.py +++ b/test/test_paths.py @@ -59,77 +59,66 @@ class LinuxTestCases(FakeSystem): SYS_NAME = "Linux" def test_both_xdg_and_fallback_dirs(self): - self.assertEqual( - confuse.config_dirs(), - ["/home/test/.config", "/home/test/xdgconfig", "/etc/xdg", "/etc"], - ) + assert confuse.config_dirs() == [ + "/home/test/.config", + "/home/test/xdgconfig", + "/etc/xdg", + "/etc", + ] def test_fallback_only(self): del os.environ["XDG_CONFIG_HOME"] - self.assertEqual( - confuse.config_dirs(), ["/home/test/.config", "/etc/xdg", "/etc"] - ) + assert confuse.config_dirs() == ["/home/test/.config", "/etc/xdg", "/etc"] def test_xdg_matching_fallback_not_duplicated(self): os.environ["XDG_CONFIG_HOME"] = "~/.config" - self.assertEqual( - confuse.config_dirs(), ["/home/test/.config", "/etc/xdg", "/etc"] - ) + assert confuse.config_dirs() == ["/home/test/.config", "/etc/xdg", "/etc"] def test_xdg_config_dirs(self): os.environ["XDG_CONFIG_DIRS"] = "/usr/local/etc/xdg:/etc/xdg" - self.assertEqual( - confuse.config_dirs(), - [ - "/home/test/.config", - "/home/test/xdgconfig", - "/usr/local/etc/xdg", - "/etc/xdg", - "/etc", - ], - ) + assert confuse.config_dirs() == [ + "/home/test/.config", + "/home/test/xdgconfig", + "/usr/local/etc/xdg", + "/etc/xdg", + "/etc", + ] class OSXTestCases(FakeSystem): SYS_NAME = "Darwin" def test_mac_dirs(self): - self.assertEqual( - confuse.config_dirs(), - [ - "/Users/test/.config", - "/Users/test/Library/Application Support", - "/etc/xdg", - "/etc", - ], - ) + assert confuse.config_dirs() == [ + "/Users/test/.config", + "/Users/test/Library/Application Support", + "/etc/xdg", + "/etc", + ] def test_xdg_config_dirs(self): os.environ["XDG_CONFIG_DIRS"] = "/usr/local/etc/xdg:/etc/xdg" - self.assertEqual( - confuse.config_dirs(), - [ - "/Users/test/.config", - "/Users/test/Library/Application Support", - "/usr/local/etc/xdg", - "/etc/xdg", - "/etc", - ], - ) + assert confuse.config_dirs() == [ + "/Users/test/.config", + "/Users/test/Library/Application Support", + "/usr/local/etc/xdg", + "/etc/xdg", + "/etc", + ] class WindowsTestCases(FakeSystem): SYS_NAME = "Windows" def test_dir_from_environ(self): - self.assertEqual( - confuse.config_dirs(), - ["C:\\Users\\test\\AppData\\Roaming", "C:\\Users\\test\\winconfig"], - ) + assert confuse.config_dirs() == [ + "C:\\Users\\test\\AppData\\Roaming", + "C:\\Users\\test\\winconfig", + ] def test_fallback_dir(self): del os.environ["APPDATA"] - self.assertEqual(confuse.config_dirs(), ["C:\\Users\\test\\AppData\\Roaming"]) + assert confuse.config_dirs() == ["C:\\Users\\test\\AppData\\Roaming"] class ConfigFilenamesTest(unittest.TestCase): @@ -144,7 +133,7 @@ def tearDown(self): def test_no_sources_when_files_missing(self): config = confuse.Configuration("myapp", read=False) filenames = [s.filename for s in config.sources] - self.assertEqual(filenames, []) + assert filenames == [] def test_search_package(self): config = confuse.Configuration("myapp", __name__, read=False) @@ -157,30 +146,29 @@ def test_search_package(self): else: self.fail("no default source") - self.assertEqual( - default_source.filename, - os.path.join(os.path.dirname(__file__), "config_default.yaml"), + assert default_source.filename == os.path.join( + os.path.dirname(__file__), "config_default.yaml" ) - self.assertTrue(source.default) + assert source.default class EnvVarTest(FakeSystem): TMP_HOME = True def setUp(self): - super(EnvVarTest, self).setUp() + super().setUp() self.config = confuse.Configuration("myapp", read=False) os.environ["MYAPPDIR"] = self.home # use the tmp home as a config dir def test_env_var_name(self): - self.assertEqual(self.config._env_var, "MYAPPDIR") + assert self.config._env_var == "MYAPPDIR" def test_env_var_dir_has_first_priority(self): - self.assertEqual(self.config.config_dir(), self.home) + assert self.config.config_dir() == self.home def test_env_var_missing(self): del os.environ["MYAPPDIR"] - self.assertNotEqual(self.config.config_dir(), self.home) + assert self.config.config_dir() != self.home class PrimaryConfigDirTest(FakeSystem): @@ -199,7 +187,7 @@ def makedirs(self, path, *args, **kwargs): os.path = os_path def setUp(self): - super(PrimaryConfigDirTest, self).setUp() + super().setUp() if hasattr(self, "join"): os.path.join = self.join os.makedirs, self._makedirs = self.makedirs, os.makedirs @@ -207,7 +195,7 @@ def setUp(self): self.config = confuse.Configuration("test", read=False) def tearDown(self): - super(PrimaryConfigDirTest, self).tearDown() + super().tearDown() if hasattr(self, "_makedirs"): os.makedirs = self._makedirs @@ -215,14 +203,14 @@ def test_create_dir_if_none_exists(self): path = os.path.join(self.home, ".config", "test") assert not os.path.exists(path) - self.assertEqual(self.config.config_dir(), path) - self.assertTrue(os.path.isdir(path)) + assert self.config.config_dir() == path + assert os.path.isdir(path) def test_return_existing_dir(self): path = os.path.join(self.home, "xdgconfig", "test") os.makedirs(path) _touch(os.path.join(path, confuse.CONFIG_FILENAME)) - self.assertEqual(self.config.config_dir(), path) + assert self.config.config_dir() == path def test_do_not_create_dir_if_lower_priority_exists(self): path1 = os.path.join(self.home, "xdgconfig", "test") @@ -232,6 +220,6 @@ def test_do_not_create_dir_if_lower_priority_exists(self): assert not os.path.exists(path1) assert os.path.exists(path2) - self.assertEqual(self.config.config_dir(), path2) - self.assertFalse(os.path.isdir(path1)) - self.assertTrue(os.path.isdir(path2)) + assert self.config.config_dir() == path2 + assert not os.path.isdir(path1) + assert os.path.isdir(path2) diff --git a/test/test_utils.py b/test/test_utils.py index 632b3e3..94a3387 100644 --- a/test/test_utils.py +++ b/test/test_utils.py @@ -2,6 +2,8 @@ from argparse import Namespace from collections import OrderedDict +import pytest + import confuse @@ -9,68 +11,68 @@ class BuildDictTests(unittest.TestCase): def test_pure_dicts(self): config = {"foo": {"bar": 1}} result = confuse.util.build_dict(config) - self.assertEqual(1, result["foo"]["bar"]) + assert 1 == result["foo"]["bar"] def test_namespaces(self): config = Namespace(foo=Namespace(bar=2), another=1) result = confuse.util.build_dict(config) - self.assertEqual(2, result["foo"]["bar"]) - self.assertEqual(1, result["another"]) + assert 2 == result["foo"]["bar"] + assert 1 == result["another"] def test_dot_sep_keys(self): config = {"foo.bar": 1} result = confuse.util.build_dict(config.copy()) - self.assertEqual(1, result["foo.bar"]) + assert 1 == result["foo.bar"] result = confuse.util.build_dict(config.copy(), sep=".") - self.assertEqual(1, result["foo"]["bar"]) + assert 1 == result["foo"]["bar"] def test_dot_sep_keys_clobber(self): args = [("foo.bar", 1), ("foo.bar.zar", 2)] config = OrderedDict(args) result = confuse.util.build_dict(config.copy(), sep=".") - self.assertEqual({"zar": 2}, result["foo"]["bar"]) - self.assertEqual(2, result["foo"]["bar"]["zar"]) + assert {"zar": 2} == result["foo"]["bar"] + assert 2 == result["foo"]["bar"]["zar"] # Reverse and do it again! (should be stable) args.reverse() config = OrderedDict(args) result = confuse.util.build_dict(config.copy(), sep=".") - self.assertEqual({"zar": 2}, result["foo"]["bar"]) - self.assertEqual(2, result["foo"]["bar"]["zar"]) + assert {"zar": 2} == result["foo"]["bar"] + assert 2 == result["foo"]["bar"]["zar"] def test_dot_sep_keys_no_clobber(self): args = [("foo.bar", 1), ("foo.far", 2), ("foo.zar.dar", 4)] config = OrderedDict(args) result = confuse.util.build_dict(config.copy(), sep=".") - self.assertEqual(1, result["foo"]["bar"]) - self.assertEqual(2, result["foo"]["far"]) - self.assertEqual(4, result["foo"]["zar"]["dar"]) + assert 1 == result["foo"]["bar"] + assert 2 == result["foo"]["far"] + assert 4 == result["foo"]["zar"]["dar"] def test_adjacent_underscores_sep_keys(self): config = {"foo__bar_baz": 1} result = confuse.util.build_dict(config.copy()) - self.assertEqual(1, result["foo__bar_baz"]) + assert 1 == result["foo__bar_baz"] result = confuse.util.build_dict(config.copy(), sep="_") - self.assertEqual(1, result["foo"][""]["bar"]["baz"]) + assert 1 == result["foo"][""]["bar"]["baz"] result = confuse.util.build_dict(config.copy(), sep="__") - self.assertEqual(1, result["foo"]["bar_baz"]) + assert 1 == result["foo"]["bar_baz"] def test_keep_none(self): config = {"foo": None} result = confuse.util.build_dict(config.copy()) - with self.assertRaises(KeyError): + with pytest.raises(KeyError): result["foo"] result = confuse.util.build_dict(config.copy(), keep_none=True) - self.assertIs(None, result["foo"]) + assert None is result["foo"] def test_keep_none_with_nested(self): config = {"foo": {"bar": None}} result = confuse.util.build_dict(config.copy()) - self.assertEqual({}, result["foo"]) + assert {} == result["foo"] result = confuse.util.build_dict(config.copy(), keep_none=True) - self.assertIs(None, result["foo"]["bar"]) + assert None is result["foo"]["bar"] diff --git a/test/test_valid.py b/test/test_valid.py index 623712a..fad61a6 100644 --- a/test/test_valid.py +++ b/test/test_valid.py @@ -3,6 +3,8 @@ import unittest from collections.abc import Mapping, Sequence +import pytest + import confuse from . import _root @@ -12,59 +14,59 @@ class ValidConfigTest(unittest.TestCase): def test_validate_simple_dict(self): config = _root({"foo": 5}) valid = config.get({"foo": confuse.Integer()}) - self.assertEqual(valid["foo"], 5) + assert valid["foo"] == 5 def test_default_value(self): config = _root({}) valid = config.get({"foo": confuse.Integer(8)}) - self.assertEqual(valid["foo"], 8) + assert valid["foo"] == 8 def test_undeclared_key_raises_keyerror(self): config = _root({"foo": 5}) valid = config.get({"foo": confuse.Integer()}) - with self.assertRaises(KeyError): + with pytest.raises(KeyError): valid["bar"] def test_undeclared_key_ignored_from_input(self): config = _root({"foo": 5, "bar": 6}) valid = config.get({"foo": confuse.Integer()}) - with self.assertRaises(KeyError): + with pytest.raises(KeyError): valid["bar"] def test_int_template_shortcut(self): config = _root({"foo": 5}) valid = config.get({"foo": int}) - self.assertEqual(valid["foo"], 5) + assert valid["foo"] == 5 def test_int_default_shortcut(self): config = _root({}) valid = config.get({"foo": 9}) - self.assertEqual(valid["foo"], 9) + assert valid["foo"] == 9 def test_attribute_access(self): config = _root({"foo": 5}) valid = config.get({"foo": confuse.Integer()}) - self.assertEqual(valid.foo, 5) + assert valid.foo == 5 def test_missing_required_value_raises_error_on_validate(self): config = _root({}) - with self.assertRaises(confuse.NotFoundError): + with pytest.raises(confuse.NotFoundError): config.get({"foo": confuse.Integer()}) def test_none_as_default(self): config = _root({}) valid = config.get({"foo": confuse.Integer(None)}) - self.assertIsNone(valid["foo"]) + assert valid["foo"] is None def test_wrong_type_raises_error_on_validate(self): config = _root({"foo": "bar"}) - with self.assertRaises(confuse.ConfigTypeError): + with pytest.raises(confuse.ConfigTypeError): config.get({"foo": confuse.Integer()}) def test_validate_individual_value(self): config = _root({"foo": 5}) valid = config["foo"].get(confuse.Integer()) - self.assertEqual(valid, 5) + assert valid == 5 def test_nested_dict_template(self): config = _root( @@ -77,7 +79,7 @@ def test_nested_dict_template(self): "foo": {"bar": confuse.Integer()}, } ) - self.assertEqual(valid["foo"]["bar"], 9) + assert valid["foo"]["bar"] == 9 def test_nested_attribute_access(self): config = _root( @@ -90,137 +92,137 @@ def test_nested_attribute_access(self): "foo": {"bar": confuse.Integer()}, } ) - self.assertEqual(valid.foo.bar, 8) + assert valid.foo.bar == 8 class AsTemplateTest(unittest.TestCase): def test_plain_int_as_template(self): typ = confuse.as_template(int) - self.assertIsInstance(typ, confuse.Integer) - self.assertEqual(typ.default, confuse.REQUIRED) + assert isinstance(typ, confuse.Integer) + assert typ.default == confuse.REQUIRED def test_concrete_int_as_template(self): typ = confuse.as_template(2) - self.assertIsInstance(typ, confuse.Integer) - self.assertEqual(typ.default, 2) + assert isinstance(typ, confuse.Integer) + assert typ.default == 2 def test_plain_string_as_template(self): typ = confuse.as_template(str) - self.assertIsInstance(typ, confuse.String) - self.assertEqual(typ.default, confuse.REQUIRED) + assert isinstance(typ, confuse.String) + assert typ.default == confuse.REQUIRED def test_concrete_string_as_template(self): typ = confuse.as_template("foo") - self.assertIsInstance(typ, confuse.String) - self.assertEqual(typ.default, "foo") + assert isinstance(typ, confuse.String) + assert typ.default == "foo" def test_dict_as_template(self): typ = confuse.as_template({"key": 9}) - self.assertIsInstance(typ, confuse.MappingTemplate) - self.assertIsInstance(typ.subtemplates["key"], confuse.Integer) - self.assertEqual(typ.subtemplates["key"].default, 9) + assert isinstance(typ, confuse.MappingTemplate) + assert isinstance(typ.subtemplates["key"], confuse.Integer) + assert typ.subtemplates["key"].default == 9 def test_nested_dict_as_template(self): typ = confuse.as_template({"outer": {"inner": 2}}) - self.assertIsInstance(typ, confuse.MappingTemplate) - self.assertIsInstance(typ.subtemplates["outer"], confuse.MappingTemplate) - self.assertIsInstance( + assert isinstance(typ, confuse.MappingTemplate) + assert isinstance(typ.subtemplates["outer"], confuse.MappingTemplate) + assert isinstance( typ.subtemplates["outer"].subtemplates["inner"], confuse.Integer ) - self.assertEqual(typ.subtemplates["outer"].subtemplates["inner"].default, 2) + assert typ.subtemplates["outer"].subtemplates["inner"].default == 2 def test_list_as_template(self): typ = confuse.as_template(list()) - self.assertIsInstance(typ, confuse.OneOf) - self.assertEqual(typ.default, confuse.REQUIRED) + assert isinstance(typ, confuse.OneOf) + assert typ.default == confuse.REQUIRED def test_set_as_template(self): typ = confuse.as_template(set()) - self.assertIsInstance(typ, confuse.Choice) + assert isinstance(typ, confuse.Choice) def test_enum_type_as_template(self): typ = confuse.as_template(enum.Enum) - self.assertIsInstance(typ, confuse.Choice) + assert isinstance(typ, confuse.Choice) def test_float_type_as_tempalte(self): typ = confuse.as_template(float) - self.assertIsInstance(typ, confuse.Number) - self.assertEqual(typ.default, confuse.REQUIRED) + assert isinstance(typ, confuse.Number) + assert typ.default == confuse.REQUIRED def test_concrete_float_as_template(self): typ = confuse.as_template(2.0) - self.assertIsInstance(typ, confuse.Number) - self.assertEqual(typ.default, 2.0) + assert isinstance(typ, confuse.Number) + assert typ.default == 2.0 def test_none_as_template(self): typ = confuse.as_template(None) - self.assertIs(type(typ), confuse.Template) - self.assertEqual(typ.default, None) + assert type(typ) is confuse.Template + assert typ.default is None def test_required_as_template(self): typ = confuse.as_template(confuse.REQUIRED) - self.assertIs(type(typ), confuse.Template) - self.assertEqual(typ.default, confuse.REQUIRED) + assert type(typ) is confuse.Template + assert typ.default == confuse.REQUIRED def test_dict_type_as_template(self): typ = confuse.as_template(dict) - self.assertIsInstance(typ, confuse.TypeTemplate) - self.assertEqual(typ.typ, Mapping) - self.assertEqual(typ.default, confuse.REQUIRED) + assert isinstance(typ, confuse.TypeTemplate) + assert typ.typ == Mapping + assert typ.default == confuse.REQUIRED def test_list_type_as_template(self): typ = confuse.as_template(list) - self.assertIsInstance(typ, confuse.TypeTemplate) - self.assertEqual(typ.typ, Sequence) - self.assertEqual(typ.default, confuse.REQUIRED) + assert isinstance(typ, confuse.TypeTemplate) + assert typ.typ == Sequence + assert typ.default == confuse.REQUIRED def test_set_type_as_template(self): typ = confuse.as_template(set) - self.assertIsInstance(typ, confuse.TypeTemplate) - self.assertEqual(typ.typ, set) - self.assertEqual(typ.default, confuse.REQUIRED) + assert isinstance(typ, confuse.TypeTemplate) + assert typ.typ is set + assert typ.default == confuse.REQUIRED def test_other_type_as_template(self): class MyClass: pass typ = confuse.as_template(MyClass) - self.assertIsInstance(typ, confuse.TypeTemplate) - self.assertEqual(typ.typ, MyClass) - self.assertEqual(typ.default, confuse.REQUIRED) + assert isinstance(typ, confuse.TypeTemplate) + assert typ.typ == MyClass + assert typ.default == confuse.REQUIRED class StringTemplateTest(unittest.TestCase): def test_validate_string(self): config = _root({"foo": "bar"}) valid = config.get({"foo": confuse.String()}) - self.assertEqual(valid["foo"], "bar") + assert valid["foo"] == "bar" def test_string_default_value(self): config = _root({}) valid = config.get({"foo": confuse.String("baz")}) - self.assertEqual(valid["foo"], "baz") + assert valid["foo"] == "baz" def test_pattern_matching(self): config = _root({"foo": "bar", "baz": "zab"}) valid = config.get({"foo": confuse.String(pattern="^ba.$")}) - self.assertEqual(valid["foo"], "bar") - with self.assertRaises(confuse.ConfigValueError): + assert valid["foo"] == "bar" + with pytest.raises(confuse.ConfigValueError): config.get({"baz": confuse.String(pattern="!")}) def test_string_template_shortcut(self): config = _root({"foo": "bar"}) valid = config.get({"foo": str}) - self.assertEqual(valid["foo"], "bar") + assert valid["foo"] == "bar" def test_string_default_shortcut(self): config = _root({}) valid = config.get({"foo": "bar"}) - self.assertEqual(valid["foo"], "bar") + assert valid["foo"] == "bar" def test_check_string_type(self): config = _root({"foo": 5}) - with self.assertRaises(confuse.ConfigTypeError): + with pytest.raises(confuse.ConfigTypeError): config.get({"foo": confuse.String()}) @@ -228,18 +230,18 @@ class NumberTest(unittest.TestCase): def test_validate_int_as_number(self): config = _root({"foo": 2}) valid = config["foo"].get(confuse.Number()) - self.assertIsInstance(valid, int) - self.assertEqual(valid, 2) + assert isinstance(valid, int) + assert valid == 2 def test_validate_float_as_number(self): config = _root({"foo": 3.0}) valid = config["foo"].get(confuse.Number()) - self.assertIsInstance(valid, float) - self.assertEqual(valid, 3.0) + assert isinstance(valid, float) + assert valid == 3.0 def test_validate_string_as_number(self): config = _root({"foo": "bar"}) - with self.assertRaises(confuse.ConfigTypeError): + with pytest.raises(confuse.ConfigTypeError): config["foo"].get(confuse.Number()) @@ -247,21 +249,21 @@ class ChoiceTest(unittest.TestCase): def test_validate_good_choice_in_list(self): config = _root({"foo": 2}) valid = config["foo"].get(confuse.Choice([1, 2, 4, 8, 16])) - self.assertEqual(valid, 2) + assert valid == 2 def test_validate_bad_choice_in_list(self): config = _root({"foo": 3}) - with self.assertRaises(confuse.ConfigValueError): + with pytest.raises(confuse.ConfigValueError): config["foo"].get(confuse.Choice([1, 2, 4, 8, 16])) def test_validate_good_choice_in_dict(self): config = _root({"foo": 2}) valid = config["foo"].get(confuse.Choice({2: "two", 4: "four"})) - self.assertEqual(valid, "two") + assert valid == "two" def test_validate_bad_choice_in_dict(self): config = _root({"foo": 3}) - with self.assertRaises(confuse.ConfigValueError): + with pytest.raises(confuse.ConfigValueError): config["foo"].get(confuse.Choice({2: "two", 4: "four"})) @@ -269,7 +271,7 @@ class OneOfTest(unittest.TestCase): def test_default_value(self): config = _root({}) valid = config["foo"].get(confuse.OneOf([], default="bar")) - self.assertEqual(valid, "bar") + assert valid == "bar" def test_validate_good_choice_in_list(self): config = _root({"foo": 2}) @@ -281,7 +283,7 @@ def test_validate_good_choice_in_list(self): ] ) ) - self.assertEqual(valid, 2) + assert valid == 2 def test_validate_first_good_choice_in_list(self): config = _root({"foo": 3.14}) @@ -293,11 +295,11 @@ def test_validate_first_good_choice_in_list(self): ] ) ) - self.assertEqual(valid, 3) + assert valid == 3 def test_validate_no_choice_in_list(self): config = _root({"foo": None}) - with self.assertRaises(confuse.ConfigValueError): + with pytest.raises(confuse.ConfigValueError): config["foo"].get( confuse.OneOf( [ @@ -312,7 +314,7 @@ class BadTemplate: pass config = _root({}) - with self.assertRaises(confuse.ConfigTemplateError): + with pytest.raises(confuse.ConfigTemplateError): config.get(confuse.OneOf([BadTemplate()])) del BadTemplate @@ -321,26 +323,26 @@ class StrSeqTest(unittest.TestCase): def test_string_list(self): config = _root({"foo": ["bar", "baz"]}) valid = config["foo"].get(confuse.StrSeq()) - self.assertEqual(valid, ["bar", "baz"]) + assert valid == ["bar", "baz"] def test_string_tuple(self): config = _root({"foo": ("bar", "baz")}) valid = config["foo"].get(confuse.StrSeq()) - self.assertEqual(valid, ["bar", "baz"]) + assert valid == ["bar", "baz"] def test_whitespace_separated_string(self): config = _root({"foo": "bar baz"}) valid = config["foo"].get(confuse.StrSeq()) - self.assertEqual(valid, ["bar", "baz"]) + assert valid == ["bar", "baz"] def test_invalid_type(self): config = _root({"foo": 9}) - with self.assertRaises(confuse.ConfigTypeError): + with pytest.raises(confuse.ConfigTypeError): config["foo"].get(confuse.StrSeq()) def test_invalid_sequence_type(self): config = _root({"foo": ["bar", 2126]}) - with self.assertRaises(confuse.ConfigTypeError): + with pytest.raises(confuse.ConfigTypeError): config["foo"].get(confuse.StrSeq()) @@ -348,41 +350,41 @@ class FilenameTest(unittest.TestCase): def test_default_value(self): config = _root({}) valid = config["foo"].get(confuse.Filename("foo/bar")) - self.assertEqual(valid, "foo/bar") + assert valid == "foo/bar" def test_default_none(self): config = _root({}) valid = config["foo"].get(confuse.Filename(None)) - self.assertEqual(valid, None) + assert valid is None def test_missing_required_value(self): config = _root({}) - with self.assertRaises(confuse.NotFoundError): + with pytest.raises(confuse.NotFoundError): config["foo"].get(confuse.Filename()) def test_filename_relative_to_working_dir(self): config = _root({"foo": "bar"}) valid = config["foo"].get(confuse.Filename(cwd="/dev/null")) - self.assertEqual(valid, os.path.realpath("/dev/null/bar")) + assert valid == os.path.realpath("/dev/null/bar") def test_filename_relative_to_sibling(self): config = _root({"foo": "/", "bar": "baz"}) valid = config.get( {"foo": confuse.Filename(), "bar": confuse.Filename(relative_to="foo")} ) - self.assertEqual(valid.foo, os.path.realpath("/")) - self.assertEqual(valid.bar, os.path.realpath("/baz")) + assert valid.foo == os.path.realpath("/") + assert valid.bar == os.path.realpath("/baz") def test_filename_working_dir_overrides_sibling(self): config = _root({"foo": "bar"}) valid = config.get( {"foo": confuse.Filename(cwd="/dev/null", relative_to="baz")} ) - self.assertEqual(valid.foo, os.path.realpath("/dev/null/bar")) + assert valid.foo == os.path.realpath("/dev/null/bar") def test_filename_relative_to_sibling_with_recursion(self): config = _root({"foo": "/", "bar": "r", "baz": "z"}) - with self.assertRaises(confuse.ConfigTemplateError): + with pytest.raises(confuse.ConfigTemplateError): config.get( { "foo": confuse.Filename(relative_to="bar"), @@ -393,30 +395,30 @@ def test_filename_relative_to_sibling_with_recursion(self): def test_filename_relative_to_self(self): config = _root({"foo": "bar"}) - with self.assertRaises(confuse.ConfigTemplateError): + with pytest.raises(confuse.ConfigTemplateError): config.get({"foo": confuse.Filename(relative_to="foo")}) def test_filename_relative_to_sibling_needs_siblings(self): config = _root({"foo": "bar"}) - with self.assertRaises(confuse.ConfigTemplateError): + with pytest.raises(confuse.ConfigTemplateError): config["foo"].get(confuse.Filename(relative_to="bar")) def test_filename_relative_to_sibling_needs_template(self): config = _root({"foo": "/", "bar": "baz"}) - with self.assertRaises(confuse.ConfigTemplateError): + with pytest.raises(confuse.ConfigTemplateError): config.get({"bar": confuse.Filename(relative_to="foo")}) def test_filename_with_non_file_source(self): config = _root({"foo": "foo/bar"}) valid = config["foo"].get(confuse.Filename()) - self.assertEqual(valid, os.path.join(os.getcwd(), "foo", "bar")) + assert valid == os.path.join(os.getcwd(), "foo", "bar") def test_filename_with_file_source(self): source = confuse.ConfigSource({"foo": "foo/bar"}, filename="/baz/config.yaml") config = _root(source) config.config_dir = lambda: "/config/path" valid = config["foo"].get(confuse.Filename()) - self.assertEqual(valid, os.path.realpath("/config/path/foo/bar")) + assert valid == os.path.realpath("/config/path/foo/bar") def test_filename_with_default_source(self): source = confuse.ConfigSource( @@ -425,7 +427,7 @@ def test_filename_with_default_source(self): config = _root(source) config.config_dir = lambda: "/config/path" valid = config["foo"].get(confuse.Filename()) - self.assertEqual(valid, os.path.realpath("/config/path/foo/bar")) + assert valid == os.path.realpath("/config/path/foo/bar") def test_filename_use_config_source_dir(self): source = confuse.ConfigSource( @@ -434,28 +436,28 @@ def test_filename_use_config_source_dir(self): config = _root(source) config.config_dir = lambda: "/config/path" valid = config["foo"].get(confuse.Filename()) - self.assertEqual(valid, os.path.realpath("/baz/foo/bar")) + assert valid == os.path.realpath("/baz/foo/bar") def test_filename_in_source_dir(self): source = confuse.ConfigSource({"foo": "foo/bar"}, filename="/baz/config.yaml") config = _root(source) config.config_dir = lambda: "/config/path" valid = config["foo"].get(confuse.Filename(in_source_dir=True)) - self.assertEqual(valid, os.path.realpath("/baz/foo/bar")) + assert valid == os.path.realpath("/baz/foo/bar") def test_filename_in_source_dir_overrides_in_app_dir(self): source = confuse.ConfigSource({"foo": "foo/bar"}, filename="/baz/config.yaml") config = _root(source) config.config_dir = lambda: "/config/path" valid = config["foo"].get(confuse.Filename(in_source_dir=True, in_app_dir=True)) - self.assertEqual(valid, os.path.realpath("/baz/foo/bar")) + assert valid == os.path.realpath("/baz/foo/bar") def test_filename_in_app_dir_non_file_source(self): source = confuse.ConfigSource({"foo": "foo/bar"}) config = _root(source) config.config_dir = lambda: "/config/path" valid = config["foo"].get(confuse.Filename(in_app_dir=True)) - self.assertEqual(valid, os.path.realpath("/config/path/foo/bar")) + assert valid == os.path.realpath("/config/path/foo/bar") def test_filename_in_app_dir_overrides_config_source_dir(self): source = confuse.ConfigSource( @@ -464,11 +466,11 @@ def test_filename_in_app_dir_overrides_config_source_dir(self): config = _root(source) config.config_dir = lambda: "/config/path" valid = config["foo"].get(confuse.Filename(in_app_dir=True)) - self.assertEqual(valid, os.path.realpath("/config/path/foo/bar")) + assert valid == os.path.realpath("/config/path/foo/bar") def test_filename_wrong_type(self): config = _root({"foo": 8}) - with self.assertRaises(confuse.ConfigTypeError): + with pytest.raises(confuse.ConfigTypeError): config["foo"].get(confuse.Filename()) @@ -478,23 +480,23 @@ def test_path_value(self): config = _root({"foo": "foo/bar"}) valid = config["foo"].get(confuse.Path()) - self.assertEqual(valid, pathlib.Path(os.path.abspath("foo/bar"))) + assert valid == pathlib.Path(os.path.abspath("foo/bar")) def test_default_value(self): import pathlib config = _root({}) valid = config["foo"].get(confuse.Path("foo/bar")) - self.assertEqual(valid, pathlib.Path("foo/bar")) + assert valid == pathlib.Path("foo/bar") def test_default_none(self): config = _root({}) valid = config["foo"].get(confuse.Path(None)) - self.assertEqual(valid, None) + assert valid is None def test_missing_required_value(self): config = _root({}) - with self.assertRaises(confuse.NotFoundError): + with pytest.raises(confuse.NotFoundError): config["foo"].get(confuse.Path()) @@ -502,151 +504,145 @@ class BaseTemplateTest(unittest.TestCase): def test_base_template_accepts_any_value(self): config = _root({"foo": 4.2}) valid = config["foo"].get(confuse.Template()) - self.assertEqual(valid, 4.2) + assert valid == 4.2 def test_base_template_required(self): config = _root({}) - with self.assertRaises(confuse.NotFoundError): + with pytest.raises(confuse.NotFoundError): config["foo"].get(confuse.Template()) def test_base_template_with_default(self): config = _root({}) valid = config["foo"].get(confuse.Template("bar")) - self.assertEqual(valid, "bar") + assert valid == "bar" class TypeTemplateTest(unittest.TestCase): def test_correct_type(self): config = _root({"foo": set()}) valid = config["foo"].get(confuse.TypeTemplate(set)) - self.assertEqual(valid, set()) + assert valid == set() def test_incorrect_type(self): config = _root({"foo": dict()}) - with self.assertRaises(confuse.ConfigTypeError): + with pytest.raises(confuse.ConfigTypeError): config["foo"].get(confuse.TypeTemplate(set)) def test_missing_required_value(self): config = _root({}) - with self.assertRaises(confuse.NotFoundError): + with pytest.raises(confuse.NotFoundError): config["foo"].get(confuse.TypeTemplate(set)) def test_default_value(self): config = _root({}) - valid = config["foo"].get(confuse.TypeTemplate(set, set([1, 2]))) - self.assertEqual(valid, set([1, 2])) + valid = config["foo"].get(confuse.TypeTemplate(set, {1, 2})) + assert valid == {1, 2} class SequenceTest(unittest.TestCase): def test_int_list(self): config = _root({"foo": [1, 2, 3]}) valid = config["foo"].get(confuse.Sequence(int)) - self.assertEqual(valid, [1, 2, 3]) + assert valid == [1, 2, 3] def test_dict_list(self): config = _root({"foo": [{"bar": 1, "baz": 2}, {"bar": 3, "baz": 4}]}) valid = config["foo"].get(confuse.Sequence({"bar": int, "baz": int})) - self.assertEqual(valid, [{"bar": 1, "baz": 2}, {"bar": 3, "baz": 4}]) + assert valid == [{"bar": 1, "baz": 2}, {"bar": 3, "baz": 4}] def test_invalid_item(self): config = _root({"foo": [{"bar": 1, "baz": 2}, {"bar": 3, "bak": 4}]}) - with self.assertRaises(confuse.NotFoundError): + with pytest.raises(confuse.NotFoundError): config["foo"].get(confuse.Sequence({"bar": int, "baz": int})) def test_wrong_type(self): config = _root({"foo": {"one": 1, "two": 2, "three": 3}}) - with self.assertRaises(confuse.ConfigTypeError): + with pytest.raises(confuse.ConfigTypeError): config["foo"].get(confuse.Sequence(int)) def test_missing(self): config = _root({"foo": [1, 2, 3]}) valid = config["bar"].get(confuse.Sequence(int)) - self.assertEqual(valid, []) + assert valid == [] class MappingValuesTest(unittest.TestCase): def test_int_dict(self): config = _root({"foo": {"one": 1, "two": 2, "three": 3}}) valid = config["foo"].get(confuse.MappingValues(int)) - self.assertEqual(valid, {"one": 1, "two": 2, "three": 3}) + assert valid == {"one": 1, "two": 2, "three": 3} def test_dict_dict(self): config = _root( {"foo": {"first": {"bar": 1, "baz": 2}, "second": {"bar": 3, "baz": 4}}} ) valid = config["foo"].get(confuse.MappingValues({"bar": int, "baz": int})) - self.assertEqual( - valid, - { - "first": {"bar": 1, "baz": 2}, - "second": {"bar": 3, "baz": 4}, - }, - ) + assert valid == {"first": {"bar": 1, "baz": 2}, "second": {"bar": 3, "baz": 4}} def test_invalid_item(self): config = _root( {"foo": {"first": {"bar": 1, "baz": 2}, "second": {"bar": 3, "bak": 4}}} ) - with self.assertRaises(confuse.NotFoundError): + with pytest.raises(confuse.NotFoundError): config["foo"].get(confuse.MappingValues({"bar": int, "baz": int})) def test_wrong_type(self): config = _root({"foo": [1, 2, 3]}) - with self.assertRaises(confuse.ConfigTypeError): + with pytest.raises(confuse.ConfigTypeError): config["foo"].get(confuse.MappingValues(int)) def test_missing(self): config = _root({"foo": {"one": 1, "two": 2, "three": 3}}) valid = config["bar"].get(confuse.MappingValues(int)) - self.assertEqual(valid, {}) + assert valid == {} class OptionalTest(unittest.TestCase): def test_optional_string_valid_type(self): config = _root({"foo": "bar"}) valid = config["foo"].get(confuse.Optional(confuse.String())) - self.assertEqual(valid, "bar") + assert valid == "bar" def test_optional_string_invalid_type(self): config = _root({"foo": 5}) - with self.assertRaises(confuse.ConfigTypeError): + with pytest.raises(confuse.ConfigTypeError): config["foo"].get(confuse.Optional(confuse.String())) def test_optional_string_null(self): config = _root({"foo": None}) valid = config["foo"].get(confuse.Optional(confuse.String())) - self.assertIsNone(valid) + assert valid is None def test_optional_string_null_default_value(self): config = _root({"foo": None}) valid = config["foo"].get(confuse.Optional(confuse.String(), "baz")) - self.assertEqual(valid, "baz") + assert valid == "baz" def test_optional_string_null_string_provides_default(self): config = _root({"foo": None}) valid = config["foo"].get(confuse.Optional(confuse.String("baz"))) - self.assertEqual(valid, "baz") + assert valid == "baz" def test_optional_string_null_string_default_override(self): config = _root({"foo": None}) valid = config["foo"].get( confuse.Optional(confuse.String("baz"), default="bar") ) - self.assertEqual(valid, "bar") + assert valid == "bar" def test_optional_string_allow_missing_no_explicit_default(self): config = _root({}) valid = config["foo"].get(confuse.Optional(confuse.String())) - self.assertIsNone(valid) + assert valid is None def test_optional_string_allow_missing_default_value(self): config = _root({}) valid = config["foo"].get(confuse.Optional(confuse.String(), "baz")) - self.assertEqual(valid, "baz") + assert valid == "baz" def test_optional_string_missing_not_allowed(self): config = _root({}) - with self.assertRaises(confuse.NotFoundError): + with pytest.raises(confuse.NotFoundError): config["foo"].get(confuse.Optional(confuse.String(), allow_missing=False)) def test_optional_string_null_missing_not_allowed(self): @@ -654,53 +650,53 @@ def test_optional_string_null_missing_not_allowed(self): valid = config["foo"].get( confuse.Optional(confuse.String(), allow_missing=False) ) - self.assertIsNone(valid) + assert valid is None def test_optional_mapping_template_valid(self): config = _root({"foo": {"bar": 5, "baz": "bak"}}) template = {"bar": confuse.Integer(), "baz": confuse.String()} valid = config.get({"foo": confuse.Optional(template)}) - self.assertEqual(valid["foo"]["bar"], 5) - self.assertEqual(valid["foo"]["baz"], "bak") + assert valid["foo"]["bar"] == 5 + assert valid["foo"]["baz"] == "bak" def test_optional_mapping_template_invalid(self): config = _root({"foo": {"bar": 5, "baz": 10}}) template = {"bar": confuse.Integer(), "baz": confuse.String()} - with self.assertRaises(confuse.ConfigTypeError): + with pytest.raises(confuse.ConfigTypeError): config.get({"foo": confuse.Optional(template)}) def test_optional_mapping_template_null(self): config = _root({"foo": None}) template = {"bar": confuse.Integer(), "baz": confuse.String()} valid = config.get({"foo": confuse.Optional(template)}) - self.assertIsNone(valid["foo"]) + assert valid["foo"] is None def test_optional_mapping_template_null_default_value(self): config = _root({"foo": None}) template = {"bar": confuse.Integer(), "baz": confuse.String()} valid = config.get({"foo": confuse.Optional(template, {})}) - self.assertIsInstance(valid["foo"], dict) + assert isinstance(valid["foo"], dict) def test_optional_mapping_template_allow_missing_no_explicit_default(self): config = _root({}) template = {"bar": confuse.Integer(), "baz": confuse.String()} valid = config.get({"foo": confuse.Optional(template)}) - self.assertIsNone(valid["foo"]) + assert valid["foo"] is None def test_optional_mapping_template_allow_missing_default_value(self): config = _root({}) template = {"bar": confuse.Integer(), "baz": confuse.String()} valid = config.get({"foo": confuse.Optional(template, {})}) - self.assertIsInstance(valid["foo"], dict) + assert isinstance(valid["foo"], dict) def test_optional_mapping_template_missing_not_allowed(self): config = _root({}) template = {"bar": confuse.Integer(), "baz": confuse.String()} - with self.assertRaises(confuse.NotFoundError): + with pytest.raises(confuse.NotFoundError): config.get({"foo": confuse.Optional(template, allow_missing=False)}) def test_optional_mapping_template_null_missing_not_allowed(self): config = _root({"foo": None}) template = {"bar": confuse.Integer(), "baz": confuse.String()} valid = config.get({"foo": confuse.Optional(template, allow_missing=False)}) - self.assertIsNone(valid["foo"]) + assert valid["foo"] is None diff --git a/test/test_validation.py b/test/test_validation.py index f31127a..793af12 100644 --- a/test/test_validation.py +++ b/test/test_validation.py @@ -2,6 +2,8 @@ import os import unittest +import pytest + import confuse from . import _root @@ -11,21 +13,21 @@ class TypeCheckTest(unittest.TestCase): def test_str_type_correct(self): config = _root({"foo": "bar"}) value = config["foo"].get(str) - self.assertEqual(value, "bar") + assert value == "bar" def test_str_type_incorrect(self): config = _root({"foo": 2}) - with self.assertRaises(confuse.ConfigTypeError): + with pytest.raises(confuse.ConfigTypeError): config["foo"].get(str) def test_int_type_correct(self): config = _root({"foo": 2}) value = config["foo"].get(int) - self.assertEqual(value, 2) + assert value == 2 def test_int_type_incorrect(self): config = _root({"foo": "bar"}) - with self.assertRaises(confuse.ConfigTypeError): + with pytest.raises(confuse.ConfigTypeError): config["foo"].get(int) @@ -33,14 +35,14 @@ class BuiltInValidatorTest(unittest.TestCase): def test_as_filename_with_non_file_source(self): config = _root({"foo": "foo/bar"}) value = config["foo"].as_filename() - self.assertEqual(value, os.path.join(os.getcwd(), "foo", "bar")) + assert value == os.path.join(os.getcwd(), "foo", "bar") def test_as_filename_with_file_source(self): source = confuse.ConfigSource({"foo": "foo/bar"}, filename="/baz/config.yaml") config = _root(source) config.config_dir = lambda: "/config/path" value = config["foo"].as_filename() - self.assertEqual(value, os.path.realpath("/config/path/foo/bar")) + assert value == os.path.realpath("/config/path/foo/bar") def test_as_filename_with_default_source(self): source = confuse.ConfigSource( @@ -49,11 +51,11 @@ def test_as_filename_with_default_source(self): config = _root(source) config.config_dir = lambda: "/config/path" value = config["foo"].as_filename() - self.assertEqual(value, os.path.realpath("/config/path/foo/bar")) + assert value == os.path.realpath("/config/path/foo/bar") def test_as_filename_wrong_type(self): config = _root({"foo": None}) - with self.assertRaises(confuse.ConfigTypeError): + with pytest.raises(confuse.ConfigTypeError): config["foo"].as_filename() def test_as_path(self): @@ -62,21 +64,21 @@ def test_as_path(self): try: import pathlib except ImportError: - with self.assertRaises(ImportError): + with pytest.raises(ImportError): value = config["foo"].as_path() else: value = config["foo"].as_path() path = pathlib.Path(path) - self.assertEqual(value, path) + assert value == path def test_as_choice_correct(self): config = _root({"foo": "bar"}) value = config["foo"].as_choice(["foo", "bar", "baz"]) - self.assertEqual(value, "bar") + assert value == "bar" def test_as_choice_error(self): config = _root({"foo": "bar"}) - with self.assertRaises(confuse.ConfigValueError): + with pytest.raises(confuse.ConfigValueError): config["foo"].as_choice(["foo", "baz"]) def test_as_choice_with_dict(self): @@ -87,7 +89,7 @@ def test_as_choice_with_dict(self): "x": "y", } ) - self.assertEqual(res, "baz") + assert res == "baz" def test_as_choice_with_enum(self): class Foobar(enum.Enum): @@ -95,14 +97,14 @@ class Foobar(enum.Enum): config = _root({"foo": Foobar.Foo.value}) res = config["foo"].as_choice(Foobar) - self.assertEqual(res, Foobar.Foo) + assert res == Foobar.Foo def test_as_choice_with_enum_error(self): class Foobar(enum.Enum): Foo = "bar" config = _root({"foo": "foo"}) - with self.assertRaises(confuse.ConfigValueError): + with pytest.raises(confuse.ConfigValueError): config["foo"].as_choice(Foobar) def test_as_number_float(self): @@ -115,16 +117,16 @@ def test_as_number_int(self): def test_as_number_string(self): config = _root({"s": "a"}) - with self.assertRaises(confuse.ConfigTypeError): + with pytest.raises(confuse.ConfigTypeError): config["s"].as_number() def test_as_str_seq_str(self): config = _root({"k": "a b c"}) - self.assertEqual(config["k"].as_str_seq(), ["a", "b", "c"]) + assert config["k"].as_str_seq() == ["a", "b", "c"] def test_as_str_seq_list(self): config = _root({"k": ["a b", "c"]}) - self.assertEqual(config["k"].as_str_seq(), ["a b", "c"]) + assert config["k"].as_str_seq() == ["a b", "c"] def test_as_str(self): config = _root({"s": "foo"}) @@ -132,14 +134,14 @@ def test_as_str(self): def test_as_str_non_string(self): config = _root({"f": 1.0}) - with self.assertRaises(confuse.ConfigTypeError): + with pytest.raises(confuse.ConfigTypeError): config["f"].as_str() def test_as_str_expanded(self): config = _root({"s": "${CONFUSE_TEST_VAR}/bar"}) os.environ["CONFUSE_TEST_VAR"] = "foo" - self.assertEqual(config["s"].as_str_expanded(), "foo/bar") + assert config["s"].as_str_expanded() == "foo/bar" def test_as_pairs(self): config = _root({"k": [{"a": "A"}, "b", ["c", "C"]]}) - self.assertEqual([("a", "A"), ("b", None), ("c", "C")], config["k"].as_pairs()) + assert [("a", "A"), ("b", None), ("c", "C")] == config["k"].as_pairs() diff --git a/test/test_views.py b/test/test_views.py index 6f5d6b0..ddd825e 100644 --- a/test/test_views.py +++ b/test/test_views.py @@ -1,5 +1,7 @@ import unittest +import pytest + import confuse from . import _root @@ -9,81 +11,81 @@ class SingleSourceTest(unittest.TestCase): def test_dict_access(self): config = _root({"foo": "bar"}) value = config["foo"].get() - self.assertEqual(value, "bar") + assert value == "bar" def test_list_access(self): config = _root({"foo": ["bar", "baz"]}) value = config["foo"][1].get() - self.assertEqual(value, "baz") + assert value == "baz" def test_missing_key(self): config = _root({"foo": "bar"}) - with self.assertRaises(confuse.NotFoundError): + with pytest.raises(confuse.NotFoundError): config["baz"].get() def test_missing_index(self): config = _root({"l": ["foo", "bar"]}) - with self.assertRaises(confuse.NotFoundError): + with pytest.raises(confuse.NotFoundError): config["l"][5].get() def test_dict_iter(self): config = _root({"foo": "bar", "baz": "qux"}) keys = [key for key in config] - self.assertEqual(set(keys), set(["foo", "baz"])) + assert set(keys) == {"foo", "baz"} def test_list_iter(self): config = _root({"l": ["foo", "bar"]}) items = [subview.get() for subview in config["l"]] - self.assertEqual(items, ["foo", "bar"]) + assert items == ["foo", "bar"] def test_int_iter(self): config = _root({"n": 2}) - with self.assertRaises(confuse.ConfigTypeError): + with pytest.raises(confuse.ConfigTypeError): [item for item in config["n"]] def test_dict_keys(self): config = _root({"foo": "bar", "baz": "qux"}) keys = config.keys() - self.assertEqual(set(keys), set(["foo", "baz"])) + assert set(keys) == {"foo", "baz"} def test_dict_values(self): config = _root({"foo": "bar", "baz": "qux"}) values = [value.get() for value in config.values()] - self.assertEqual(set(values), set(["bar", "qux"])) + assert set(values) == {"bar", "qux"} def test_dict_items(self): config = _root({"foo": "bar", "baz": "qux"}) items = [(key, value.get()) for (key, value) in config.items()] - self.assertEqual(set(items), set([("foo", "bar"), ("baz", "qux")])) + assert set(items) == {("foo", "bar"), ("baz", "qux")} def test_list_keys_error(self): config = _root({"l": ["foo", "bar"]}) - with self.assertRaises(confuse.ConfigTypeError): + with pytest.raises(confuse.ConfigTypeError): config["l"].keys() def test_list_sequence(self): config = _root({"l": ["foo", "bar"]}) items = [item.get() for item in config["l"].sequence()] - self.assertEqual(items, ["foo", "bar"]) + assert items == ["foo", "bar"] def test_dict_sequence_error(self): config = _root({"foo": "bar", "baz": "qux"}) - with self.assertRaises(confuse.ConfigTypeError): + with pytest.raises(confuse.ConfigTypeError): list(config.sequence()) def test_dict_contents(self): config = _root({"foo": "bar", "baz": "qux"}) contents = config.all_contents() - self.assertEqual(set(contents), set(["foo", "baz"])) + assert set(contents) == {"foo", "baz"} def test_list_contents(self): config = _root({"l": ["foo", "bar"]}) contents = config["l"].all_contents() - self.assertEqual(list(contents), ["foo", "bar"]) + assert list(contents) == ["foo", "bar"] def test_int_contents(self): config = _root({"n": 2}) - with self.assertRaises(confuse.ConfigTypeError): + with pytest.raises(confuse.ConfigTypeError): list(config["n"].all_contents()) @@ -91,175 +93,175 @@ class ConverstionTest(unittest.TestCase): def test_str_conversion_from_str(self): config = _root({"foo": "bar"}) value = str(config["foo"]) - self.assertEqual(value, "bar") + assert value == "bar" def test_str_conversion_from_int(self): config = _root({"foo": 2}) value = str(config["foo"]) - self.assertEqual(value, "2") + assert value == "2" def test_bool_conversion_from_bool(self): config = _root({"foo": True}) value = bool(config["foo"]) - self.assertEqual(value, True) + assert value def test_bool_conversion_from_int(self): config = _root({"foo": 0}) value = bool(config["foo"]) - self.assertEqual(value, False) + assert not value class NameTest(unittest.TestCase): def test_root_name(self): config = _root() - self.assertEqual(config.name, "root") + assert config.name == "root" def test_string_access_name(self): config = _root() name = config["foo"].name - self.assertEqual(name, "foo") + assert name == "foo" def test_int_access_name(self): config = _root() name = config[5].name - self.assertEqual(name, "#5") + assert name == "#5" def test_nested_access_name(self): config = _root() name = config[5]["foo"]["bar"][20].name - self.assertEqual(name, "#5.foo.bar#20") + assert name == "#5.foo.bar#20" class MultipleSourceTest(unittest.TestCase): def test_dict_access_shadowed(self): config = _root({"foo": "bar"}, {"foo": "baz"}) value = config["foo"].get() - self.assertEqual(value, "bar") + assert value == "bar" def test_dict_access_fall_through(self): config = _root({"qux": "bar"}, {"foo": "baz"}) value = config["foo"].get() - self.assertEqual(value, "baz") + assert value == "baz" def test_dict_access_missing(self): config = _root({"qux": "bar"}, {"foo": "baz"}) - with self.assertRaises(confuse.NotFoundError): + with pytest.raises(confuse.NotFoundError): config["fred"].get() def test_list_access_shadowed(self): config = _root({"l": ["a", "b"]}, {"l": ["c", "d", "e"]}) value = config["l"][1].get() - self.assertEqual(value, "b") + assert value == "b" def test_list_access_fall_through(self): config = _root({"l": ["a", "b"]}, {"l": ["c", "d", "e"]}) value = config["l"][2].get() - self.assertEqual(value, "e") + assert value == "e" def test_list_access_missing(self): config = _root({"l": ["a", "b"]}, {"l": ["c", "d", "e"]}) - with self.assertRaises(confuse.NotFoundError): + with pytest.raises(confuse.NotFoundError): config["l"][3].get() def test_access_dict_replaced(self): config = _root({"foo": {"bar": "baz"}}, {"foo": {"qux": "fred"}}) value = config["foo"].get() - self.assertEqual(value, {"bar": "baz"}) + assert value == {"bar": "baz"} def test_dict_keys_merged(self): config = _root({"foo": {"bar": "baz"}}, {"foo": {"qux": "fred"}}) keys = config["foo"].keys() - self.assertEqual(set(keys), set(["bar", "qux"])) + assert set(keys) == {"bar", "qux"} def test_dict_keys_replaced(self): config = _root({"foo": {"bar": "baz"}}, {"foo": {"bar": "fred"}}) keys = config["foo"].keys() - self.assertEqual(list(keys), ["bar"]) + assert list(keys) == ["bar"] def test_dict_values_merged(self): config = _root({"foo": {"bar": "baz"}}, {"foo": {"qux": "fred"}}) values = [value.get() for value in config["foo"].values()] - self.assertEqual(set(values), set(["baz", "fred"])) + assert set(values) == {"baz", "fred"} def test_dict_values_replaced(self): config = _root({"foo": {"bar": "baz"}}, {"foo": {"bar": "fred"}}) values = [value.get() for value in config["foo"].values()] - self.assertEqual(list(values), ["baz"]) + assert list(values) == ["baz"] def test_dict_items_merged(self): config = _root({"foo": {"bar": "baz"}}, {"foo": {"qux": "fred"}}) items = [(key, value.get()) for (key, value) in config["foo"].items()] - self.assertEqual(set(items), set([("bar", "baz"), ("qux", "fred")])) + assert set(items) == {("bar", "baz"), ("qux", "fred")} def test_dict_items_replaced(self): config = _root({"foo": {"bar": "baz"}}, {"foo": {"bar": "fred"}}) items = [(key, value.get()) for (key, value) in config["foo"].items()] - self.assertEqual(list(items), [("bar", "baz")]) + assert list(items) == [("bar", "baz")] def test_list_sequence_shadowed(self): config = _root({"l": ["a", "b"]}, {"l": ["c", "d", "e"]}) items = [item.get() for item in config["l"].sequence()] - self.assertEqual(items, ["a", "b"]) + assert items == ["a", "b"] def test_list_sequence_shadowed_by_dict(self): config = _root({"foo": {"bar": "baz"}}, {"foo": ["qux", "fred"]}) - with self.assertRaises(confuse.ConfigTypeError): + with pytest.raises(confuse.ConfigTypeError): list(config["foo"].sequence()) def test_dict_contents_concatenated(self): config = _root({"foo": {"bar": "baz"}}, {"foo": {"qux": "fred"}}) contents = config["foo"].all_contents() - self.assertEqual(set(contents), set(["bar", "qux"])) + assert set(contents) == {"bar", "qux"} def test_dict_contents_concatenated_not_replaced(self): config = _root({"foo": {"bar": "baz"}}, {"foo": {"bar": "fred"}}) contents = config["foo"].all_contents() - self.assertEqual(list(contents), ["bar", "bar"]) + assert list(contents) == ["bar", "bar"] def test_list_contents_concatenated(self): config = _root({"foo": ["bar", "baz"]}, {"foo": ["qux", "fred"]}) contents = config["foo"].all_contents() - self.assertEqual(list(contents), ["bar", "baz", "qux", "fred"]) + assert list(contents) == ["bar", "baz", "qux", "fred"] def test_int_contents_error(self): config = _root({"foo": ["bar", "baz"]}, {"foo": 5}) - with self.assertRaises(confuse.ConfigTypeError): + with pytest.raises(confuse.ConfigTypeError): list(config["foo"].all_contents()) def test_list_and_dict_contents_concatenated(self): config = _root({"foo": ["bar", "baz"]}, {"foo": {"qux": "fred"}}) contents = config["foo"].all_contents() - self.assertEqual(list(contents), ["bar", "baz", "qux"]) + assert list(contents) == ["bar", "baz", "qux"] def test_add_source(self): config = _root({"foo": "bar"}) config.add({"baz": "qux"}) - self.assertEqual(config["foo"].get(), "bar") - self.assertEqual(config["baz"].get(), "qux") + assert config["foo"].get() == "bar" + assert config["baz"].get() == "qux" class SetTest(unittest.TestCase): def test_set_missing_top_level_key(self): config = _root({}) config["foo"] = "bar" - self.assertEqual(config["foo"].get(), "bar") + assert config["foo"].get() == "bar" def test_override_top_level_key(self): config = _root({"foo": "bar"}) config["foo"] = "baz" - self.assertEqual(config["foo"].get(), "baz") + assert config["foo"].get() == "baz" def test_set_second_level_key(self): config = _root({}) config["foo"]["bar"] = "baz" - self.assertEqual(config["foo"]["bar"].get(), "baz") + assert config["foo"]["bar"].get() == "baz" def test_override_second_level_key(self): config = _root({"foo": {"bar": "qux"}}) config["foo"]["bar"] = "baz" - self.assertEqual(config["foo"]["bar"].get(), "baz") + assert config["foo"]["bar"].get() == "baz" def test_override_list_index(self): config = _root({"foo": ["a", "b", "c"]}) config["foo"][1] = "bar" - self.assertEqual(config["foo"][1].get(), "bar") + assert config["foo"][1].get() == "bar" diff --git a/test/test_yaml.py b/test/test_yaml.py index 1c4fa7d..ea30409 100644 --- a/test/test_yaml.py +++ b/test/test_yaml.py @@ -1,5 +1,6 @@ import unittest +import pytest import yaml import confuse @@ -14,12 +15,12 @@ def load(s): class ParseTest(unittest.TestCase): def test_dict_parsed_as_ordereddict(self): v = load("a: b\nc: d") - self.assertTrue(isinstance(v, confuse.OrderedDict)) - self.assertEqual(list(v), ["a", "c"]) + assert isinstance(v, confuse.OrderedDict) + assert list(v) == ["a", "c"] def test_string_beginning_with_percent(self): v = load("foo: %bar") - self.assertEqual(v["foo"], "%bar") + assert v["foo"] == "%bar" class FileParseTest(unittest.TestCase): @@ -30,101 +31,85 @@ def _parse_contents(self, contents): def test_load_file(self): v = self._parse_contents(b"foo: bar") - self.assertEqual(v["foo"], "bar") + assert v["foo"] == "bar" def test_syntax_error(self): - try: + with pytest.raises(confuse.ConfigError, match=r"test_config\.yaml"): self._parse_contents(b":") - except confuse.ConfigError as exc: - self.assertTrue("test_config.yaml" in exc.name) - else: - self.fail("ConfigError not raised") def test_reload_conf(self): with TempDir() as temp: path = temp.sub("test_config.yaml", b"foo: bar") config = confuse.Configuration("test", __name__) config.set_file(filename=path) - self.assertEqual(config["foo"].get(), "bar") + assert config["foo"].get() == "bar" temp.sub("test_config.yaml", b"foo: bar2\ntest: hello world") config.reload() - self.assertEqual(config["foo"].get(), "bar2") - self.assertEqual(config["test"].get(), "hello world") + assert config["foo"].get() == "bar2" + assert config["test"].get() == "hello world" def test_tab_indentation_error(self): - try: + with pytest.raises(confuse.ConfigError, match="found tab"): self._parse_contents(b"foo:\n\tbar: baz") - except confuse.ConfigError as exc: - self.assertTrue("found tab" in exc.args[0]) - else: - self.fail("ConfigError not raised") class StringParseTest(unittest.TestCase): def test_load_string(self): v = confuse.load_yaml_string("foo: bar", "test") - self.assertEqual(v["foo"], "bar") + assert v["foo"] == "bar" def test_string_syntax_error(self): - try: + with pytest.raises(confuse.ConfigError, match="test"): confuse.load_yaml_string(":", "test") - except confuse.ConfigError as exc: - self.assertTrue("test" in exc.name) - else: - self.fail("ConfigError not raised") def test_string_tab_indentation_error(self): - try: + with pytest.raises(confuse.ConfigError, match="found tab"): confuse.load_yaml_string("foo:\n\tbar: baz", "test") - except confuse.ConfigError as exc: - self.assertTrue("found tab" in exc.args[0]) - else: - self.fail("ConfigError not raised") class ParseAsScalarTest(unittest.TestCase): def test_text_string(self): v = confuse.yaml_util.parse_as_scalar("foo", confuse.Loader) - self.assertEqual(v, "foo") + assert v == "foo" def test_number_string_to_int(self): v = confuse.yaml_util.parse_as_scalar("1", confuse.Loader) - self.assertIsInstance(v, int) - self.assertEqual(v, 1) + assert isinstance(v, int) + assert v == 1 def test_number_string_to_float(self): v = confuse.yaml_util.parse_as_scalar("1.0", confuse.Loader) - self.assertIsInstance(v, float) - self.assertEqual(v, 1.0) + assert isinstance(v, float) + assert v == 1.0 def test_bool_string_to_bool(self): v = confuse.yaml_util.parse_as_scalar("true", confuse.Loader) - self.assertIs(v, True) + assert v is True def test_empty_string_to_none(self): v = confuse.yaml_util.parse_as_scalar("", confuse.Loader) - self.assertIs(v, None) + assert v is None def test_null_string_to_none(self): v = confuse.yaml_util.parse_as_scalar("null", confuse.Loader) - self.assertIs(v, None) + assert v is None def test_dict_string_unchanged(self): v = confuse.yaml_util.parse_as_scalar('{"foo": "bar"}', confuse.Loader) - self.assertEqual(v, '{"foo": "bar"}') + assert v == '{"foo": "bar"}' def test_dict_unchanged(self): v = confuse.yaml_util.parse_as_scalar({"foo": "bar"}, confuse.Loader) - self.assertEqual(v, {"foo": "bar"}) + assert v == {"foo": "bar"} def test_list_string_unchanged(self): v = confuse.yaml_util.parse_as_scalar('["foo", "bar"]', confuse.Loader) - self.assertEqual(v, '["foo", "bar"]') + assert v == '["foo", "bar"]' def test_list_unchanged(self): v = confuse.yaml_util.parse_as_scalar(["foo", "bar"], confuse.Loader) - self.assertEqual(v, ["foo", "bar"]) + assert v == ["foo", "bar"] def test_invalid_yaml_string_unchanged(self): v = confuse.yaml_util.parse_as_scalar("!", confuse.Loader) - self.assertEqual(v, "!") + assert v == "!" From f9577d23ca42033ad475fb8c1a6c7a6f5f213b2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= Date: Sat, 27 Dec 2025 15:11:04 +0000 Subject: [PATCH 04/15] Configure mypy --- confuse/sources.py | 2 +- confuse/templates.py | 2 +- poetry.lock | 12 ++++++++ pyproject.toml | 1 + setup.cfg | 3 ++ test/test_paths.py | 65 ++++++++++++++++---------------------------- 6 files changed, 42 insertions(+), 43 deletions(-) diff --git a/confuse/sources.py b/confuse/sources.py index 41d244b..a4961e5 100644 --- a/confuse/sources.py +++ b/confuse/sources.py @@ -4,7 +4,7 @@ from .util import build_dict -class ConfigSource(dict): +class ConfigSource(dict[str, object]): """A dictionary augmented with metadata about the source of the configuration. """ diff --git a/confuse/templates.py b/confuse/templates.py index 5094dfe..4c4c830 100644 --- a/confuse/templates.py +++ b/confuse/templates.py @@ -630,7 +630,7 @@ def convert(self, value, view): return value -class AttrDict(dict): +class AttrDict(dict[str, object]): """A `dict` subclass that can be accessed via attributes (dot notation) for convenience. """ diff --git a/poetry.lock b/poetry.lock index 8d8c44e..edf04f7 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1721,6 +1721,18 @@ files = [ ] markers = {main = "extra == \"docs\" and python_version < \"3.11\"", dev = "python_full_version <= \"3.11.0a6\"", lint = "python_version < \"3.11\""} +[[package]] +name = "types-pyyaml" +version = "6.0.12.20250915" +description = "Typing stubs for PyYAML" +optional = false +python-versions = ">=3.9" +groups = ["lint"] +files = [ + {file = "types_pyyaml-6.0.12.20250915-py3-none-any.whl", hash = "sha256:e7d4d9e064e89a3b3cae120b4990cd370874d2bf12fa5f46c97018dd5d3c9ab6"}, + {file = "types_pyyaml-6.0.12.20250915.tar.gz", hash = "sha256:0f8b54a528c303f0e6f7165687dd33fafa81c807fcac23f632b63aa624ced1d3"}, +] + [[package]] name = "typing-extensions" version = "4.15.0" diff --git a/pyproject.toml b/pyproject.toml index 704889a..2a333fe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,7 @@ lint = [ "mypy >= 1.18.2", "ruff >= 0.6.4", "sphinx-lint >= 1.0.0", + "types-pyyaml >=6.0.12", ] [project.optional-dependencies] diff --git a/setup.cfg b/setup.cfg index af6d9bf..d83a97c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -16,3 +16,6 @@ exclude_also = [coverage:html] show_contexts = true + +[mypy] +allow_any_generics = false diff --git a/test/test_paths.py b/test/test_paths.py index 15e5ce1..d31bf44 100644 --- a/test/test_paths.py +++ b/test/test_paths.py @@ -5,6 +5,7 @@ import shutil import tempfile import unittest +from typing import ClassVar import confuse import confuse.yaml_util @@ -29,30 +30,33 @@ def _touch(path): open(path, "a").close() -class FakeSystem(unittest.TestCase): - SYS_NAME = None - TMP_HOME = False - +class FakeHome(unittest.TestCase): def setUp(self): - if self.TMP_HOME: - self.home = tempfile.mkdtemp() + super().setUp() + self.home = tempfile.mkdtemp() + os.environ["HOME"] = self.home + os.environ["USERPROFILE"] = self.home - if self.SYS_NAME in SYSTEMS: - self.os_path = os.path - os.environ = {} + def tearDown(self): + super().tearDown() + shutil.rmtree(self.home) - environ, os.path = SYSTEMS[self.SYS_NAME] - os.environ.update(environ) # copy - platform.system = lambda: self.SYS_NAME - if self.TMP_HOME: - os.environ["HOME"] = self.home - os.environ["USERPROFILE"] = self.home +class FakeSystem(unittest.TestCase): + SYS_NAME: ClassVar[str] + + def setUp(self): + super().setUp() + self.os_path = os.path + os.environ = {} + + environ, os.path = SYSTEMS[self.SYS_NAME] + os.environ.update(environ) # copy + platform.system = lambda: self.SYS_NAME def tearDown(self): + super().tearDown() platform.system, os.environ, os.path = DEFAULT - if hasattr(self, "home"): - shutil.rmtree(self.home) class LinuxTestCases(FakeSystem): @@ -152,9 +156,7 @@ def test_search_package(self): assert source.default -class EnvVarTest(FakeSystem): - TMP_HOME = True - +class EnvVarTest(FakeHome): def setUp(self): super().setUp() self.config = confuse.Configuration("myapp", read=False) @@ -171,34 +173,15 @@ def test_env_var_missing(self): assert self.config.config_dir() != self.home -class PrimaryConfigDirTest(FakeSystem): +@unittest.skipUnless(os.system == "Linux", "Linux-specific tests") +class PrimaryConfigDirTest(FakeHome, FakeSystem): SYS_NAME = "Linux" # conversion from posix to nt is easy - TMP_HOME = True - - if platform.system() == "Windows": - # wrap these functions as they need to work on the host system which is - # only needed on Windows as we are using `posixpath` - def join(self, *args): - return self.os_path.normpath(self.os_path.join(*args)) - - def makedirs(self, path, *args, **kwargs): - os.path, os_path = self.os_path, os.path - self._makedirs(path, *args, **kwargs) - os.path = os_path def setUp(self): super().setUp() - if hasattr(self, "join"): - os.path.join = self.join - os.makedirs, self._makedirs = self.makedirs, os.makedirs self.config = confuse.Configuration("test", read=False) - def tearDown(self): - super().tearDown() - if hasattr(self, "_makedirs"): - os.makedirs = self._makedirs - def test_create_dir_if_none_exists(self): path = os.path.join(self.home, ".config", "test") assert not os.path.exists(path) From 424a17ceeccf93d92cb2e6b4062907af858854b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= Date: Sat, 27 Dec 2025 17:34:43 +0000 Subject: [PATCH 05/15] Format docs --- .github/workflows/lint.yaml | 5 +- README.rst | 88 +++--- docs/api.rst | 4 +- docs/changelog.rst | 68 ++--- docs/dev.rst | 8 +- docs/examples.rst | 374 ++++++++++++------------ docs/index.rst | 14 +- docs/usage.rst | 551 +++++++++++++++++------------------- 8 files changed, 546 insertions(+), 566 deletions(-) diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index a4169b4..8452464 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -136,8 +136,9 @@ jobs: echo "::add-matcher::.github/problem-matchers/sphinx-build.json" echo "::add-matcher::.github/problem-matchers/sphinx-lint.json" - - name: Check docs formatting - run: poe format-docs --check + # TODO: Re-enable once https://github.com/LilSpazJoekp/docstrfmt/pull/169 is merged and released + # - name: Check docs formatting + # run: poe format-docs --check - name: Lint docs run: poe lint-docs diff --git a/README.rst b/README.rst index 7152a1a..c13f361 100644 --- a/README.rst +++ b/README.rst @@ -7,50 +7,43 @@ Confuse: painless YAML config files .. image:: http://img.shields.io/pypi/v/confuse.svg :target: https://pypi.python.org/pypi/confuse -**Confuse** is a configuration library for Python that uses `YAML`_. It takes -care of defaults, overrides, type checking, command-line integration, -environment variable support, human-readable errors, and standard OS-specific -locations. +**Confuse** is a configuration library for Python that uses YAML_. It takes care +of defaults, overrides, type checking, command-line integration, environment +variable support, human-readable errors, and standard OS-specific locations. What It Does ------------ -Here’s what Confuse brings to the table: - -- An **utterly sensible API** resembling dictionary-and-list structures - but providing **transparent validation** without lots of boilerplate - code. Type ``config['num_goats'].get(int)`` to get the configured - number of goats and ensure that it’s an integer. - -- Combine configuration data from **multiple sources**. Using - *layering*, Confuse allows user-specific configuration to seamlessly - override system-wide configuration, which in turn overrides built-in - defaults. An in-package ``config_default.yaml`` can be used to - provide bottom-layer defaults using the same syntax that users will - see. A runtime overlay allows the program to programmatically - override and add configuration values. - -- Look for configuration files in **platform-specific paths**. Like - ``$XDG_CONFIG_HOME`` or ``~/.config`` on Unix; "Application Support" on - macOS; ``%APPDATA%`` on Windows. Your program gets its own - directory, which you can use to store additional data. You can - transparently create this directory on demand if, for example, you - need to initialize the configuration file on first run. And an - environment variable can be used to override the directory's - location. - -- Integration with **command-line arguments** via `argparse`_ or `optparse`_ - from the standard library. Use argparse's declarative API to allow - command-line options to override configured defaults. - -- Include configuration values from **environment variables**. Values undergo - automatic type conversion, and nested dicts and lists are supported. +Here's what Confuse brings to the table: + +- An **utterly sensible API** resembling dictionary-and-list structures but + providing **transparent validation** without lots of boilerplate code. Type + ``config['num_goats'].get(int)`` to get the configured number of goats and + ensure that it's an integer. +- Combine configuration data from **multiple sources**. Using *layering*, + Confuse allows user-specific configuration to seamlessly override system-wide + configuration, which in turn overrides built-in defaults. An in-package + ``config_default.yaml`` can be used to provide bottom-layer defaults using the + same syntax that users will see. A runtime overlay allows the program to + programmatically override and add configuration values. +- Look for configuration files in **platform-specific paths**. Like + ``$XDG_CONFIG_HOME`` or ``~/.config`` on Unix; "Application Support" on macOS; + ``%APPDATA%`` on Windows. Your program gets its own directory, which you can + use to store additional data. You can transparently create this directory on + demand if, for example, you need to initialize the configuration file on first + run. And an environment variable can be used to override the directory's + location. +- Integration with **command-line arguments** via argparse_ or optparse_ from + the standard library. Use argparse's declarative API to allow command-line + options to override configured defaults. +- Include configuration values from **environment variables**. Values undergo + automatic type conversion, and nested dicts and lists are supported. Installation ------------ -Confuse is available on `PyPI `_ and can be installed -using :code:`pip`: +Confuse is available on `PyPI `_ and can be +installed using ``pip``: .. code-block:: sh @@ -64,14 +57,21 @@ Using Confuse Credits ------- -Confuse was made to power `beets`_. -Like beets, it is available under the `MIT license`_. +Confuse was made to power beets_. Like beets, it is available under the `MIT +license`_. -.. _ConfigParser: http://docs.python.org/library/configparser.html -.. _YAML: http://yaml.org/ -.. _optparse: http://docs.python.org/dev/library/optparse.html .. _argparse: http://docs.python.org/dev/library/argparse.html -.. _logging: http://docs.python.org/library/logging.html -.. _Confuse's documentation: http://confuse.readthedocs.org/en/latest/usage.html -.. _MIT license: http://www.opensource.org/licenses/mit-license.php + .. _beets: https://github.com/beetbox/beets + +.. _configparser: http://docs.python.org/library/configparser.html + +.. _confuse's documentation: http://confuse.readthedocs.org/en/latest/usage.html + +.. _logging: http://docs.python.org/library/logging.html + +.. _mit license: http://www.opensource.org/licenses/mit-license.php + +.. _optparse: http://docs.python.org/dev/library/optparse.html + +.. _yaml: http://yaml.org/ diff --git a/docs/api.rst b/docs/api.rst index f2902f1..4731fa9 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1,8 +1,8 @@ -================= API Documentation ================= -This part of the documentation covers the interfaces used to develop with :code:`confuse`. +This part of the documentation covers the interfaces used to develop with +``confuse``. Core ---- diff --git a/docs/changelog.rst b/docs/changelog.rst index 3d29a16..6ac33d0 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,98 +1,100 @@ Changelog ---------- +========= v2.1.1 -'''''' +------ - Include `docs` and `tests` directory in source distributions. v2.1.0 -'''''' +------ - Drop support for versions of Python below 3.9. - Removed 'u' prefix from string literals for Python 3.0+ compatibility. - Removed a number of python 2 leftovers. - Removed deprecated `pkgutil.get_loader` usage in favor of - `importlib..util.find_spec` for better compatibility with modern Python versions. -- Added typehints to `as_*` functions which allows for - enhanced type checking and IDE support. -- Added a minimal release workflow for GitHub Actions to automate the release process. + `importlib..util.find_spec` for better compatibility with modern Python + versions. +- Added typehints to `as_*` functions which allows for enhanced type checking + and IDE support. +- Added a minimal release workflow for GitHub Actions to automate the release + process. - Added support for Python 3.13 and Python 3.14. - Modernized package and tests setup. v2.0.1 -'''''' +------ - Remove a `<4` Python version requirement bound. v2.0.0 -'''''' +------ - Drop support for versions of Python below 3.6. v1.7.0 -'''''' +------ -- Add support for reading configuration values from environment variables - (see `EnvSource`). +- Add support for reading configuration values from environment variables (see + `EnvSource`). - Resolve a possible race condition when creating configuration directories. v1.6.0 -'''''' +------ -- A new `Configuration.reload` method makes it convenient to reload and - re-parse all YAML files from the file system. +- A new `Configuration.reload` method makes it convenient to reload and re-parse + all YAML files from the file system. v1.5.0 -'''''' +------ - A new `MappingValues` template behaves like `Sequence` but for mappings with arbitrary keys. - A new `Optional` template allows other templates to be null. - `Filename` templates now have an option to resolve relative to a specific - directory. Also, configuration sources now have a corresponding global - option to resolve relative to the base configuration directory instead of - the location of the specific configuration file. + directory. Also, configuration sources now have a corresponding global option + to resolve relative to the base configuration directory instead of the + location of the specific configuration file. - There is a better error message for `Sequence` templates when the data from the configuration is not a sequence. v1.4.0 -'''''' +------ - `pathlib.PurePath` objects can now be converted to `Path` templates. - `AttrDict` now properly supports (over)writing attributes via dot notation. v1.3.0 -'''''' +------ - Break up the `confuse` module into a package. (All names should still be importable from `confuse`.) - When using `None` as a template, the result is a value whose default is - `None`. Previously, this was equivalent to leaving the key off entirely, - i.e., a template with no default. To get the same effect now, use - `confuse.REQUIRED` in the template. + `None`. Previously, this was equivalent to leaving the key off entirely, i.e., + a template with no default. To get the same effect now, use `confuse.REQUIRED` + in the template. v1.2.0 -'''''' +------ -- `float` values (like ``4.2``) can now be used in templates (just like - ``42`` works as an `int` template). +- `float` values (like ``4.2``) can now be used in templates (just like ``42`` + works as an `int` template). - The `Filename` and `Path` templates now correctly accept default values. -- It's now possible to provide custom PyYAML `Loader` objects for - parsing config files. +- It's now possible to provide custom PyYAML `Loader` objects for parsing config + files. v1.1.0 -'''''' +------ -- A new ``Path`` template produces a `pathlib`_ Path object. +- A new ``Path`` template produces a pathlib_ Path object. - Drop support for Python 3.4 (following in the footsteps of PyYAML). - String templates support environment variable expansion. .. _pathlib: https://docs.python.org/3/library/pathlib.html v1.0.0 -'''''' +------ -The first stable release, and the first that `beets`_ depends on externally. +The first stable release, and the first that beets_ depends on externally. .. _beets: https://beets.io diff --git a/docs/dev.rst b/docs/dev.rst index 5fcd4b9..a507092 100644 --- a/docs/dev.rst +++ b/docs/dev.rst @@ -7,9 +7,9 @@ library. Version Bumps ------------- -This section outlines how to create a new version of the ``confuse`` library -and publish it on PyPi. The versioning follows semantic versioning principles, -where the version number is structured as ``MAJOR.MINOR.PATCH``. +This section outlines how to create a new version of the ``confuse`` library and +publish it on PyPi. The versioning follows semantic versioning principles, where +the version number is structured as ``MAJOR.MINOR.PATCH``. To create a new version, follow these steps: @@ -23,4 +23,4 @@ To create a new version, follow these steps: ``docs/changelog.rst`` file. Note: This workflow does not update the changelog version numbers; this must be -done manually before running the release workflow. \ No newline at end of file +done manually before running the release workflow. diff --git a/docs/examples.rst b/docs/examples.rst index eb0a207..450acef 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -4,17 +4,16 @@ Template Examples These examples demonstrate how the confuse templates work to validate configuration values. - Sequence -------- A ``Sequence`` template allows validation of a sequence of configuration items that all must match a subtemplate. The items in the sequence can be simple -values or more complex objects, as defined by the subtemplate. When the view -is defined in multiple sources, the highest priority source will override the +values or more complex objects, as defined by the subtemplate. When the view is +defined in multiple sources, the highest priority source will override the entire list of items, rather than appending new items to the list from lower -sources. If the view is not defined in any source of the configuration, an -empty list will be returned. +sources. If the view is not defined in any source of the configuration, an empty +list will be returned. As an example of using the ``Sequence`` template, consider a configuration that includes a list of servers, where each server is required to have a host string @@ -34,13 +33,15 @@ Validation of this configuration could be performed like this: >>> import confuse >>> import pprint ->>> source = confuse.YamlSource('servers_example.yaml') +>>> source = confuse.YamlSource("servers_example.yaml") >>> config = confuse.RootView([source]) >>> template = { -... 'servers': confuse.Sequence({ -... 'host': str, -... 'port': 80, -... }), +... "servers": confuse.Sequence( +... { +... "host": str, +... "port": 80, +... } +... ), ... } >>> valid_config = config.get(template) >>> pprint.pprint(valid_config) @@ -51,12 +52,14 @@ Validation of this configuration could be performed like this: The list of items in the initial configuration can be overridden by setting a higher priority source. Continuing the previous example: ->>> config.set({ -... 'servers': [ -... {'host': 'four.example.org'}, -... {'host': 'five.example.org', 'port': 9000}, -... ], -... }) +>>> config.set( +... { +... "servers": [ +... {"host": "four.example.org"}, +... {"host": "five.example.org", "port": 9000}, +... ], +... } +... ) >>> updated_config = config.get(template) >>> pprint.pprint(updated_config) {'servers': [{'host': 'four.example.org', 'port': 80}, @@ -68,28 +71,25 @@ If the requested view is missing, ``Sequence`` returns an empty list: >>> config.get(template) {'servers': []} -However, if an item within the sequence does not match the subtemplate -provided to ``Sequence``, then an error will be raised: +However, if an item within the sequence does not match the subtemplate provided +to ``Sequence``, then an error will be raised: ->>> config.set({ -... 'servers': [ -... {'host': 'bad_port.example.net', 'port': 'default'} -... ] -... }) +>>> config.set( +... {"servers": [{"host": "bad_port.example.net", "port": "default"}]} +... ) >>> try: ... config.get(template) ... except confuse.ConfigError as err: ... print(err) -... servers#0.port: must be a number .. note:: + A python list is not the shortcut for defining a ``Sequence`` template but will instead produce a ``OneOf`` template. For example, ``config.get([str])`` is equivalent to ``config.get(confuse.OneOf([str]))`` and *not* ``config.get(confuse.Sequence(str))``. - MappingValues ------------- @@ -97,11 +97,11 @@ A ``MappingValues`` template allows validation of a mapping of configuration items where the keys can be arbitrary but all the values need to match a subtemplate. Use cases include simple user-defined key:value pairs or larger configuration blocks that all follow the same structure, but where the keys -naming each block are user-defined. In addition, individual items in the -mapping can be overridden and new items can be added by higher priority -configuration sources. This is in contrast to the ``Sequence`` template, in -which a higher priority source overrides the entire list of configuration items -provided by a lower source. +naming each block are user-defined. In addition, individual items in the mapping +can be overridden and new items can be added by higher priority configuration +sources. This is in contrast to the ``Sequence`` template, in which a higher +priority source overrides the entire list of configuration items provided by a +lower source. In the following example, a hypothetical todo list program can be configured with user-defined colors and category labels. Colors are required to be in hex @@ -129,16 +129,18 @@ Validation of this configuration could be performed like this: >>> import confuse >>> import pprint ->>> source = confuse.YamlSource('todo_example.yaml') +>>> source = confuse.YamlSource("todo_example.yaml") >>> config = confuse.RootView([source]) >>> template = { -... 'colors': confuse.MappingValues( -... confuse.String(pattern='#[0-9a-fA-F]{6,6}') +... "colors": confuse.MappingValues( +... confuse.String(pattern="#[0-9a-fA-F]{6,6}") +... ), +... "categories": confuse.MappingValues( +... { +... "description": str, +... "priority": 0, +... } ... ), -... 'categories': confuse.MappingValues({ -... 'description': str, -... 'priority': 0, -... }), ... } >>> valid_config = config.get(template) >>> pprint.pprint(valid_config) @@ -151,22 +153,24 @@ Validation of this configuration could be performed like this: Items in the initial configuration can be overridden and the mapping can be extended by setting a higher priority source. Continuing the previous example: ->>> config.set({ -... 'colors': { -... 'green': '#008000', -... 'orange': '#FFA500', -... }, -... 'categories': { -... 'urgent': { -... 'description': 'Must get done now', -... 'priority': 100, +>>> config.set( +... { +... "colors": { +... "green": "#008000", +... "orange": "#FFA500", ... }, -... 'high': { -... 'description': 'Important, but not urgent', -... 'priority': 20, +... "categories": { +... "urgent": { +... "description": "Must get done now", +... "priority": 100, +... }, +... "high": { +... "description": "Important, but not urgent", +... "priority": 20, +... }, ... }, -... }, -... }) +... } +... ) >>> updated_config = config.get(template) >>> pprint.pprint(updated_config) {'categories': {'default': {'description': 'Things to do', 'priority': 0}, @@ -187,24 +191,24 @@ If the requested view is missing, ``MappingValues`` returns an empty dict: >>> config.get(template) {'colors': {}, 'categories': {}} -However, if an item within the mapping does not match the subtemplate -provided to ``MappingValues``, then an error will be raised: +However, if an item within the mapping does not match the subtemplate provided +to ``MappingValues``, then an error will be raised: ->>> config.set({ -... 'categories': { -... 'no_description': { -... 'priority': 10, +>>> config.set( +... { +... "categories": { +... "no_description": { +... "priority": 10, +... }, ... }, -... }, -... }) +... } +... ) >>> try: ... config.get(template) ... except confuse.ConfigError as err: ... print(err) -... categories.no_description.description not found - Filename -------- @@ -214,42 +218,43 @@ that are provided in config files are resolved relative to the application's configuration directory, as returned by ``Configuration.config_dir()``, while relative paths from command-line options are resolved from the current working directory. However, these default relative path behaviors can be changed using -the ``cwd``, ``relative_to``, ``in_app_dir``, or ``in_source_dir`` parameters -to the ``Filename`` template. In addition, relative path resolution for an -entire source file can be changed by creating a ``ConfigSource`` with the +the ``cwd``, ``relative_to``, ``in_app_dir``, or ``in_source_dir`` parameters to +the ``Filename`` template. In addition, relative path resolution for an entire +source file can be changed by creating a ``ConfigSource`` with the ``base_for_paths`` parameter set to True. Setting the behavior at the -source-level can be useful when all ``Filename`` templates should be relative -to the source. The template-level parameters provide more fine-grained control. +source-level can be useful when all ``Filename`` templates should be relative to +the source. The template-level parameters provide more fine-grained control. While the directory used for resolving relative paths can be controlled, the ``Filename`` template should not be used to guarantee that a file is contained within a given directory, because an absolute path may be provided and will not -be subject to resolution. In addition, ``Filename`` validation only ensures -that the filename is a valid path on the platform where the application is -running, not that the file or any parent directories exist or could be created. +be subject to resolution. In addition, ``Filename`` validation only ensures that +the filename is a valid path on the platform where the application is running, +not that the file or any parent directories exist or could be created. .. note:: + Running the example below will create the application config directory ``~/.config/ExampleApp/`` on MacOS and Unix machines or ``%APPDATA%\ExampleApp\`` on Windows machines. The filenames in the sample - output will also be different on your own machine because the paths to - the config files and the current working directory will be different. + output will also be different on your own machine because the paths to the + config files and the current working directory will be different. -For this example, we will validate a configuration with filenames that should -be resolved as follows: +For this example, we will validate a configuration with filenames that should be +resolved as follows: -* ``library``: a filename that should always be resolved relative to the +- ``library``: a filename that should always be resolved relative to the application's config directory -* ``media_dir``: a directory that should always be resolved relative to the +- ``media_dir``: a directory that should always be resolved relative to the source config file that provides that value -* ``photo_dir`` and ``video_dir``: subdirectories that should be resolved +- ``photo_dir`` and ``video_dir``: subdirectories that should be resolved relative of the value of ``media_dir`` -* ``temp_dir``: a directory that should be resolved relative to ``/tmp/`` -* ``log``: a filename that follows the default ``Filename`` template behavior +- ``temp_dir``: a directory that should be resolved relative to ``/tmp/`` +- ``log``: a filename that follows the default ``Filename`` template behavior The initial user config file will be at ``~/.config/ExampleApp/config.yaml``, -where it will be discovered automatically using the :ref:`Search Paths`, and -has the following contents: +where it will be discovered automatically using the :ref:`Search Paths`, and has +the following contents: .. code-block:: yaml @@ -264,16 +269,16 @@ Validation of this initial user configuration could be performed as follows: >>> import confuse >>> import pprint ->>> config = confuse.Configuration('ExampleApp', __name__) # Loads user config +>>> config = confuse.Configuration("ExampleApp", __name__) # Loads user config >>> print(config.config_dir()) # Application config directory /home/user/.config/ExampleApp >>> template = { -... 'library': confuse.Filename(in_app_dir=True), -... 'media_dir': confuse.Filename(in_source_dir=True), -... 'photo_dir': confuse.Filename(relative_to='media_dir'), -... 'video_dir': confuse.Filename(relative_to='media_dir'), -... 'temp_dir': confuse.Filename(cwd='/tmp'), -... 'log': confuse.Filename(), +... "library": confuse.Filename(in_app_dir=True), +... "media_dir": confuse.Filename(in_source_dir=True), +... "photo_dir": confuse.Filename(relative_to="media_dir"), +... "video_dir": confuse.Filename(relative_to="media_dir"), +... "temp_dir": confuse.Filename(cwd="/tmp"), +... "log": confuse.Filename(), ... } >>> valid_config = config.get(template) >>> pprint.pprint(valid_config) @@ -303,7 +308,7 @@ directory: Continuing the example code from above: ->>> config.set_file('/var/tmp/example/config.yaml') +>>> config.set_file("/var/tmp/example/config.yaml") >>> updated_config = config.get(template) >>> pprint.pprint(updated_config) {'library': '/home/user/.config/ExampleApp/new_library.db', @@ -320,10 +325,10 @@ directory because it uses the default ``Filename`` template behavior. The base directories for the ``library`` and ``temp_dir`` items are also not affected. If the previous YAML file is instead loaded with the ``base_for_paths`` -parameter set to True, then a default ``Filename`` template will use that -config file's directory as the base for resolving relative paths: +parameter set to True, then a default ``Filename`` template will use that config +file's directory as the base for resolving relative paths: ->>> config.set_file('/var/tmp/example/config.yaml', base_for_paths=True) +>>> config.set_file("/var/tmp/example/config.yaml", base_for_paths=True) >>> updated_config = config.get(template) >>> pprint.pprint(updated_config) {'library': '/home/user/.config/ExampleApp/new_library.db', @@ -350,14 +355,16 @@ by splitting a mock command line string and parsing it with ``argparse``: /home/user >>> import argparse >>> parser = argparse.ArgumentParser() ->>> parser.add_argument('--library') ->>> parser.add_argument('--media_dir') ->>> parser.add_argument('--photo_dir') ->>> parser.add_argument('--temp_dir') ->>> parser.add_argument('--log') ->>> cmd_line=('--library cmd_line_library --media_dir cmd_line_media ' -... '--photo_dir cmd_line_photo --temp_dir cmd_line_tmp ' -... '--log cmd_line_log') +>>> parser.add_argument("--library") +>>> parser.add_argument("--media_dir") +>>> parser.add_argument("--photo_dir") +>>> parser.add_argument("--temp_dir") +>>> parser.add_argument("--log") +>>> cmd_line = ( +... "--library cmd_line_library --media_dir cmd_line_media " +... "--photo_dir cmd_line_photo --temp_dir cmd_line_tmp " +... "--log cmd_line_log" +... ) >>> args = parser.parse_args(cmd_line.split()) >>> config.set_args(args) >>> config_with_cmdline = config.get(template) @@ -379,13 +386,15 @@ If a configuration value is provided as an absolute path, the path will be normalized but otherwise unchanged. Here is an example of overridding earlier values with absolute paths: ->>> config.set({ -... 'library': '~/home_library.db', -... 'media_dir': '/media', -... 'video_dir': '/video_not_under_media', -... 'temp_dir': '/var/./remove_me/..//tmp', -... 'log': '/var/log/example.log', -... }) +>>> config.set( +... { +... "library": "~/home_library.db", +... "media_dir": "/media", +... "video_dir": "/video_not_under_media", +... "temp_dir": "/var/./remove_me/..//tmp", +... "log": "/var/log/example.log", +... } +... ) >>> absolute_config = config.get(template) >>> pprint.pprint(absolute_config) {'library': '/home/user/home_library.db', @@ -401,26 +410,25 @@ the previous relative path value is now being resolved from the new ``media_dir`` absolute path. However, the ``video_dir`` was set to an absolute path and is no longer a subdirectory of ``media_dir``. - Path ---- -A ``Path`` template works the same as a ``Filename`` template, but returns -a ``pathlib.Path`` object instead of a string. Using the same initial example -as above for ``Filename`` but with ``Path`` templates gives the following: +A ``Path`` template works the same as a ``Filename`` template, but returns a +``pathlib.Path`` object instead of a string. Using the same initial example as +above for ``Filename`` but with ``Path`` templates gives the following: >>> import confuse >>> import pprint ->>> config = confuse.Configuration('ExampleApp', __name__) +>>> config = confuse.Configuration("ExampleApp", __name__) >>> print(config.config_dir()) # Application config directory /home/user/.config/ExampleApp >>> template = { -... 'library': confuse.Path(in_app_dir=True), -... 'media_dir': confuse.Path(in_source_dir=True), -... 'photo_dir': confuse.Path(relative_to='media_dir'), -... 'video_dir': confuse.Path(relative_to='media_dir'), -... 'temp_dir': confuse.Path(cwd='/tmp'), -... 'log': confuse.Path(), +... "library": confuse.Path(in_app_dir=True), +... "media_dir": confuse.Path(in_source_dir=True), +... "photo_dir": confuse.Path(relative_to="media_dir"), +... "video_dir": confuse.Path(relative_to="media_dir"), +... "temp_dir": confuse.Path(cwd="/tmp"), +... "log": confuse.Path(), ... } >>> valid_config = config.get(template) >>> pprint.pprint(valid_config) @@ -431,20 +439,19 @@ as above for ``Filename`` but with ``Path`` templates gives the following: 'temp_dir': PosixPath('/tmp/example_tmp'), 'video_dir': PosixPath('/home/user/.config/ExampleApp/media/my_videos')} - Optional -------- -While many templates like ``Integer`` and ``String`` can be configured to -return a default value if the requested view is missing, validation with these +While many templates like ``Integer`` and ``String`` can be configured to return +a default value if the requested view is missing, validation with these templates will fail if the value is left blank in the YAML file or explicitly -set to ``null`` in YAML (ie, ``None`` in python). The ``Optional`` template -can be used with other templates to allow its subtemplate to accept ``null`` -as valid and return a default value. The default behavior of ``Optional`` -allows the requested view to be missing, but this behavior can be changed by -passing ``allow_missing=False``, in which case the view must be present but its -value can still be ``null``. In all cases, any value other than ``null`` will -be passed to the subtemplate for validation, and an appropriate ``ConfigError`` +set to ``null`` in YAML (ie, ``None`` in python). The ``Optional`` template can +be used with other templates to allow its subtemplate to accept ``null`` as +valid and return a default value. The default behavior of ``Optional`` allows +the requested view to be missing, but this behavior can be changed by passing +``allow_missing=False``, in which case the view must be present but its value +can still be ``null``. In all cases, any value other than ``null`` will be +passed to the subtemplate for validation, and an appropriate ``ConfigError`` will be raised if validation fails. ``Optional`` can also be used with more complex templates like ``MappingTemplate`` to make entire sections of the configuration optional. @@ -457,16 +464,15 @@ using the ``Optional`` template with ``Filename`` as the subtemplate: >>> import sys >>> import confuse >>> def get_log_output(config): -... output = config['log'].get(confuse.Optional(confuse.Filename())) +... output = config["log"].get(confuse.Optional(confuse.Filename())) ... if output is None: ... return sys.stderr ... return output -... >>> config = confuse.RootView([]) ->>> config.set({'log': '/tmp/log.txt'}) # `log` set to a filename +>>> config.set({"log": "/tmp/log.txt"}) # `log` set to a filename >>> get_log_output(config) '/tmp/log.txt' ->>> config.set({'log': None}) # `log` set to None (ie, null in YAML) +>>> config.set({"log": None}) # `log` set to None (ie, null in YAML) >>> get_log_output(config) <_io.TextIOWrapper name='' mode='w' encoding='UTF-8'> >>> config.clear() # Clear config so that `log` is missing @@ -476,37 +482,35 @@ using the ``Optional`` template with ``Filename`` as the subtemplate: However, validation will still fail with ``Optional`` if a value is given that is invalid for the subtemplate: ->>> config.set({'log': True}) +>>> config.set({"log": True}) >>> try: ... get_log_output(config) ... except confuse.ConfigError as err: ... print(err) -... log: must be a filename, not bool And without wrapping the ``Filename`` subtemplate in ``Optional``, ``null`` values are not valid: ->>> config.set({'log': None}) +>>> config.set({"log": None}) >>> try: -... config['log'].get(confuse.Filename()) +... config["log"].get(confuse.Filename()) ... except confuse.ConfigError as err: ... print(err) -... log: must be a filename, not NoneType If a program wants to require an item to be present in the configuration, while -still allowing ``null`` to be valid, pass ``allow_missing=False`` when -creating the ``Optional`` template: +still allowing ``null`` to be valid, pass ``allow_missing=False`` when creating +the ``Optional`` template: >>> def get_log_output_no_missing(config): -... output = config['log'].get(confuse.Optional(confuse.Filename(), -... allow_missing=False)) +... output = config["log"].get( +... confuse.Optional(confuse.Filename(), allow_missing=False) +... ) ... if output is None: ... return sys.stderr ... return output -... ->>> config.set({'log': None}) # `log` set to None is still OK... +>>> config.set({"log": None}) # `log` set to None is still OK... >>> get_log_output_no_missing(config) <_io.TextIOWrapper name='' mode='w' encoding='UTF-8'> >>> config.clear() # but `log` missing now raises an error @@ -514,20 +518,19 @@ creating the ``Optional`` template: ... get_log_output_no_missing(config) ... except confuse.ConfigError as err: ... print(err) -... log not found The default value returned by ``Optional`` can be set explicitly by passing a -value to its ``default`` parameter. However, if no explicit default is passed -to ``Optional`` and the subtemplate has a default value defined, then -``Optional`` will return the subtemplate's default value. For subtemplates that -do not define default values, like ``MappingTemplate``, ``None`` will be -returned as the default unless an explicit default is provided. +value to its ``default`` parameter. However, if no explicit default is passed to +``Optional`` and the subtemplate has a default value defined, then ``Optional`` +will return the subtemplate's default value. For subtemplates that do not define +default values, like ``MappingTemplate``, ``None`` will be returned as the +default unless an explicit default is provided. In the following example, ``Optional`` is used to make an ``Integer`` template more lenient, allowing blank values to validate. In addition, the entire -``extra_config`` block can be left out without causing validation errors. If -we have a file named ``optional.yaml`` with the following contents: +``extra_config`` block can be left out without causing validation errors. If we +have a file named ``optional.yaml`` with the following contents: .. code-block:: yaml @@ -540,64 +543,67 @@ we have a file named ``optional.yaml`` with the following contents: Then the configuration can be validated as follows: >>> import confuse ->>> source = confuse.YamlSource('optional.yaml') +>>> source = confuse.YamlSource("optional.yaml") >>> config = confuse.RootView([source]) ->>> # The following `Optional` templates are all equivalent -... config['favorite_number'].get(confuse.Optional(5)) +>>> config["favorite_number"].get(confuse.Optional(5)) 5 ->>> config['favorite_number'].get(confuse.Optional(confuse.Integer(5))) +>>> config["favorite_number"].get(confuse.Optional(confuse.Integer(5))) 5 ->>> config['favorite_number'].get(confuse.Optional(int, default=5)) +>>> config["favorite_number"].get(confuse.Optional(int, default=5)) 5 ->>> # But a default passed to `Optional` takes precedence and can be any type -... config['favorite_number'].get(confuse.Optional(5, default='five')) +>>> config["favorite_number"].get(confuse.Optional(5, default="five")) 'five' ->>> # `Optional` with `MappingTemplate` returns `None` by default -... extra_config = config['extra_config'].get(confuse.Optional( -... {'fruit': str, 'number': int}, -... )) +>>> extra_config = config["extra_config"].get( +... confuse.Optional( +... {"fruit": str, "number": int}, +... ) +... ) >>> print(extra_config is None) True ->>> # But any default value can be provided, like an empty dict... -... config['extra_config'].get(confuse.Optional( -... {'fruit': str, 'number': int}, -... default={}, -... )) +>>> config["extra_config"].get( +... confuse.Optional( +... {"fruit": str, "number": int}, +... default={}, +... ) +... ) {} ->>> # or a dict with default values -... config['extra_config'].get(confuse.Optional( -... {'fruit': str, 'number': int}, -... default={'fruit': 'orange', 'number': 3}, -... )) +>>> config["extra_config"].get( +... confuse.Optional( +... {"fruit": str, "number": int}, +... default={"fruit": "orange", "number": 3}, +... ) +... ) {'fruit': 'orange', 'number': 3} -Without the ``Optional`` template wrapping the ``Integer``, the blank value -in the YAML file will cause an error: +Without the ``Optional`` template wrapping the ``Integer``, the blank value in +the YAML file will cause an error: >>> try: -... config['favorite_number'].get(5) +... config["favorite_number"].get(5) ... except confuse.ConfigError as err: ... print(err) -... favorite_number: must be a number If the ``extra_config`` for this example configuration is supplied, it must still match the subtemplate. Therefore, this will fail: ->>> config.set({'extra_config': {}}) +>>> config.set({"extra_config": {}}) >>> try: -... config['extra_config'].get(confuse.Optional( -... {'fruit': str, 'number': int}, -... )) +... config["extra_config"].get( +... confuse.Optional( +... {"fruit": str, "number": int}, +... ) +... ) ... except confuse.ConfigError as err: ... print(err) -... extra_config.fruit not found But this override of the example configuration will validate: ->>> config.set({'extra_config': {'fruit': 'banana', 'number': 1}}) ->>> config['extra_config'].get(confuse.Optional( -... {'fruit': str, 'number': int}, -... )) +>>> config.set({"extra_config": {"fruit": "banana", "number": 1}}) +>>> config["extra_config"].get( +... confuse.Optional( +... {"fruit": str, "number": int}, +... ) +... ) {'fruit': 'banana', 'number': 1} diff --git a/docs/index.rst b/docs/index.rst index 7fe84d8..8c7ac3b 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,11 +1,11 @@ .. include:: ../README.rst .. toctree:: - :maxdepth: 3 - :hidden: + :maxdepth: 3 + :hidden: - usage - examples - changelog - api - dev + usage + examples + changelog + api + dev diff --git a/docs/usage.rst b/docs/usage.rst index 1dcce78..4aa963d 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -1,172 +1,156 @@ Confuse: Painless Configuration =============================== -`Confuse`_ is a straightforward, full-featured configuration system -for Python. - -.. _Confuse: https://github.com/beetbox/confuse +Confuse_ is a straightforward, full-featured configuration system for Python. +.. _confuse: https://github.com/beetbox/confuse Basic Usage ----------- -Set up your Configuration object, which provides unified access to -all of your application’s config settings: +Set up your Configuration object, which provides unified access to all of your +application's config settings: .. code-block:: python - config = confuse.Configuration('MyGreatApp', __name__) + config = confuse.Configuration("MyGreatApp", __name__) -The first parameter is required; it’s the name of your application, which -will be used to search the system for a config file named ``config.yaml``. -See :ref:`Search Paths` for the specific locations searched. +The first parameter is required; it's the name of your application, which will +be used to search the system for a config file named ``config.yaml``. See +:ref:`Search Paths` for the specific locations searched. -The second parameter is optional: it’s the name of a module that will -guide the search for a *defaults* file. Use this if you want to include a -``config_default.yaml`` file inside your package. (The included -``example`` package does exactly this.) +The second parameter is optional: it's the name of a module that will guide the +search for a *defaults* file. Use this if you want to include a +``config_default.yaml`` file inside your package. (The included ``example`` +package does exactly this.) -Now, you can access your configuration data as if it were a simple -structure consisting of nested dicts and lists—except that you need to -call the method ``.get()`` on the leaf of this tree to get the result as -a value: +Now, you can access your configuration data as if it were a simple structure +consisting of nested dicts and lists—except that you need to call the method +``.get()`` on the leaf of this tree to get the result as a value: .. code-block:: python - value = config['foo'][2]['bar'].get() + value = config["foo"][2]["bar"].get() -Under the hood, accessing items in your configuration tree builds up a -*view* into your app’s configuration. Then, ``get()`` flattens this view -into a value, performing a search through each configuration data source -to find an answer. (More on views later.) +Under the hood, accessing items in your configuration tree builds up a *view* +into your app's configuration. Then, ``get()`` flattens this view into a value, +performing a search through each configuration data source to find an answer. +(More on views later.) -If you know that a configuration value should have a specific type, just -pass that type to ``get()``: +If you know that a configuration value should have a specific type, just pass +that type to ``get()``: .. code-block:: python - int_value = config['number_of_goats'].get(int) - -This way, Confuse will either give you an integer or raise a -``ConfigTypeError`` if the user has messed up the configuration. You’re -safe to assume after this call that ``int_value`` has the right type. If -the key doesn’t exist in any configuration file, Confuse will raise a -``NotFoundError``. Together, catching these exceptions (both subclasses -of ``confuse.ConfigError``) lets you painlessly validate the user’s -configuration as you go. + int_value = config["number_of_goats"].get(int) +This way, Confuse will either give you an integer or raise a ``ConfigTypeError`` +if the user has messed up the configuration. You're safe to assume after this +call that ``int_value`` has the right type. If the key doesn't exist in any +configuration file, Confuse will raise a ``NotFoundError``. Together, catching +these exceptions (both subclasses of ``confuse.ConfigError``) lets you +painlessly validate the user's configuration as you go. View Theory ----------- -The Confuse API is based on the concept of *views*. You can think of a -view as a *place to look* in a config file: for example, one view might -say “get the value for key ``number_of_goats``”. Another might say “get -the value at index 8 inside the sequence for key ``animal_counts``”. To -get the value for a given view, you *resolve* it by calling the -``get()`` method. - -This concept separates the specification of a location from the -mechanism for retrieving data from a location. (In this sense, it’s a -little like `XPath`_: you specify a path to data you want and *then* you -retrieve it.) - -Using views, you can write ``config['animal_counts'][8]`` and know that -no exceptions will be raised until you call ``get()``, even if the -``animal_counts`` key does not exist. More importantly, it lets you -write a single expression to search many different data sources without -preemptively merging all sources together into a single data structure. - -Views also solve an important problem with overriding collections. -Imagine, for example, that you have a dictionary called -``deliciousness`` in your config file that maps food names to tastiness -ratings. If the default configuration gives carrots a rating of 8 and -the user’s config rates them a 10, then clearly -``config['deliciousness']['carrots'].get()`` should return 10. But what -if the two data sources have different sets of vegetables? If the user -provides a value for broccoli and zucchini but not carrots, should -carrots have a default deliciousness value of 8 or should Confuse just -throw an exception? With Confuse’s views, the application gets to decide. - -The above expression, ``config['deliciousness']['carrots'].get()``, -returns 8 (falling back on the default). However, you can also write -``config['deliciousness'].get()``. This expression will cause the -*entire* user-specified mapping to override the default one, providing a -dict object like ``{'broccoli': 7, 'zucchini': 9}``. As a rule, then, -resolve a view at the same granularity you want config files to override -each other. +The Confuse API is based on the concept of *views*. You can think of a view as a +*place to look* in a config file: for example, one view might say *get the value +for key ``number_of_goats``*. Another might say *get the value at index 8 inside +the sequence for key ``animal_counts``*. To get the value for a given view, you +*resolve* it by calling the ``get()`` method. + +This concept separates the specification of a location from the mechanism for +retrieving data from a location. (In this sense, it's a little like XPath_: you +specify a path to data you want and *then* you retrieve it.) + +Using views, you can write ``config['animal_counts'][8]`` and know that no +exceptions will be raised until you call ``get()``, even if the +``animal_counts`` key does not exist. More importantly, it lets you write a +single expression to search many different data sources without preemptively +merging all sources together into a single data structure. + +Views also solve an important problem with overriding collections. Imagine, for +example, that you have a dictionary called ``deliciousness`` in your config file +that maps food names to tastiness ratings. If the default configuration gives +carrots a rating of 8 and the user's config rates them a 10, then clearly +``config['deliciousness']['carrots'].get()`` should return 10. But what if the +two data sources have different sets of vegetables? If the user provides a value +for broccoli and zucchini but not carrots, should carrots have a default +deliciousness value of 8 or should Confuse just throw an exception? With +Confuse's views, the application gets to decide. + +The above expression, ``config['deliciousness']['carrots'].get()``, returns 8 +(falling back on the default). However, you can also write +``config['deliciousness'].get()``. This expression will cause the *entire* +user-specified mapping to override the default one, providing a dict object like +``{'broccoli': 7, 'zucchini': 9}``. As a rule, then, resolve a view at the same +granularity you want config files to override each other. .. warning:: - It may appear that calling ``config.get()`` would retrieve the entire - configuration at once. However, this will return only the - *highest-priority* configuration source, masking any lower-priority - values for keys that are not present in the top source. This pitfall is - especially likely when using :ref:`Command-Line Options` or - :ref:`Environment Variables`, which may place an empty configuration - at the top of the stack. A subsequent call to ``config.get()`` might - then return no configuration at all. -.. _XPath: http://www.w3.org/TR/xpath/ + It may appear that calling ``config.get()`` would retrieve the entire + configuration at once. However, this will return only the *highest-priority* + configuration source, masking any lower-priority values for keys that are + not present in the top source. This pitfall is especially likely when using + :ref:`Command-Line Options` or :ref:`Environment Variables`, which may place + an empty configuration at the top of the stack. A subsequent call to + ``config.get()`` might then return no configuration at all. +.. _xpath: http://www.w3.org/TR/xpath/ Validation ---------- -We saw above that you can easily assert that a configuration value has a -certain type by passing that type to ``get()``. But sometimes you need -to do more than just type checking. For this reason, Confuse provides a -few methods on views that perform fancier validation or even -conversion: +We saw above that you can easily assert that a configuration value has a certain +type by passing that type to ``get()``. But sometimes you need to do more than +just type checking. For this reason, Confuse provides a few methods on views +that perform fancier validation or even conversion: -* ``as_filename()``: Normalize a filename, substituting tildes and - absolute-ifying relative paths. For filenames defined in a config file, - by default the filename is relative to the application's config directory +- ``as_filename()``: Normalize a filename, substituting tildes and + absolute-ifying relative paths. For filenames defined in a config file, by + default the filename is relative to the application's config directory (``Configuration.config_dir()``, as described below). However, if the config - file was loaded with the ``base_for_paths`` parameter set to ``True`` - (see :ref:`Manually Specifying Config Files`), then a relative path refers - to the directory containing the config file. A relative path from any other - source (e.g., command-line options) is relative to the working directory. For - full control over relative path resolution, use the ``Filename`` template - directly (see :ref:`Filename`). -* ``as_choice(choices)``: Check that a value is one of the provided - choices. The argument should be a sequence of possible values. If the - sequence is a ``dict``, then this method returns the associated value - instead of the key. -* ``as_number()``: Raise an exception unless the value is of a numeric - type. -* ``as_pairs()``: Get a collection as a list of pairs. The collection - should be a list of elements that are either pairs (i.e., two-element - lists) already or single-entry dicts. This can be helpful because, in - YAML, lists of single-element mappings have a simple syntax (``- key: - value``) and, unlike real mappings, preserve order. -* ``as_str_seq()``: Given either a string or a list of strings, return a list - of strings. A single string is split on whitespace. -* ``as_str_expanded()``: Expand any environment variables contained in - a string using `os.path.expandvars()`_. + file was loaded with the ``base_for_paths`` parameter set to ``True`` (see + :ref:`Manually Specifying Config Files`), then a relative path refers to the + directory containing the config file. A relative path from any other source + (e.g., command-line options) is relative to the working directory. For full + control over relative path resolution, use the ``Filename`` template directly + (see :ref:`Filename`). +- ``as_choice(choices)``: Check that a value is one of the provided choices. The + argument should be a sequence of possible values. If the sequence is a + ``dict``, then this method returns the associated value instead of the key. +- ``as_number()``: Raise an exception unless the value is of a numeric type. +- ``as_pairs()``: Get a collection as a list of pairs. The collection should be + a list of elements that are either pairs (i.e., two-element lists) already or + single-entry dicts. This can be helpful because, in YAML, lists of + single-element mappings have a simple syntax (``- key: value``) and, unlike + real mappings, preserve order. +- ``as_str_seq()``: Given either a string or a list of strings, return a list of + strings. A single string is split on whitespace. +- ``as_str_expanded()``: Expand any environment variables contained in a string + using `os.path.expandvars()`_. .. _os.path.expandvars(): https://docs.python.org/library/os.path.html#os.path.expandvars -For example, ``config['path'].as_filename()`` ensures that you get a -reasonable filename string from the configuration. And calling +For example, ``config['path'].as_filename()`` ensures that you get a reasonable +filename string from the configuration. And calling ``config['direction'].as_choice(['up', 'down'])`` will raise a -``ConfigValueError`` unless the ``direction`` value is either "up" or -"down". - +``ConfigValueError`` unless the ``direction`` value is either "up" or "down". Command-Line Options -------------------- -Arguments to command-line programs can be seen as just another *source* -for configuration options. Just as options in a user-specific -configuration file should override those from a system-wide config, -command-line options should take priority over all configuration files. +Arguments to command-line programs can be seen as just another *source* for +configuration options. Just as options in a user-specific configuration file +should override those from a system-wide config, command-line options should +take priority over all configuration files. -You can use the `argparse`_ and `optparse`_ modules from the standard -library with Confuse to accomplish this. Just call the ``set_args`` -method on any view and pass in the object returned by the command-line -parsing library. Values from the command-line option namespace object -will be added to the overlay for the view in question. For example, with -argparse: +You can use the argparse_ and optparse_ modules from the standard library with +Confuse to accomplish this. Just call the ``set_args`` method on any view and +pass in the object returned by the command-line parsing library. Values from the +command-line option namespace object will be added to the overlay for the view +in question. For example, with argparse: .. code-block:: python @@ -180,57 +164,53 @@ Correspondingly, with optparse: options, args = parser.parse_args() config.set_args(options) -This call will turn all of the command-line options into a top-level -source in your configuration. The key associated with each option in the -parser will become a key available in your configuration. For example, -consider this argparse script: +This call will turn all of the command-line options into a top-level source in +your configuration. The key associated with each option in the parser will +become a key available in your configuration. For example, consider this +argparse script: .. code-block:: python - config = confuse.Configuration('myapp') + config = confuse.Configuration("myapp") parser = argparse.ArgumentParser() - parser.add_argument('--foo', help='a parameter') + parser.add_argument("--foo", help="a parameter") args = parser.parse_args() config.set_args(args) - print(config['foo'].get()) + print(config["foo"].get()) -This will allow the user to override the configured value for key -``foo`` by passing ``--foo `` on the command line. +This will allow the user to override the configured value for key ``foo`` by +passing ``--foo `` on the command line. -Overriding nested values can be accomplished by passing `dots=True` and -have dot-delimited properties on the incoming object. +Overriding nested values can be accomplished by passing `dots=True` and have +dot-delimited properties on the incoming object. .. code-block:: python - parser.add_argument('--bar', help='nested parameter', dest='foo.bar') + parser.add_argument("--bar", help="nested parameter", dest="foo.bar") args = parser.parse_args() # args looks like: {'foo.bar': 'value'} config.set_args(args, dots=True) - print(config['foo']['bar'].get()) + print(config["foo"]["bar"].get()) `set_args` works with generic dictionaries too. .. code-block:: python - args = { - 'foo': { - 'bar': 1 - } - } + args = {"foo": {"bar": 1}} config.set_args(args, dots=True) - print(config['foo']['bar'].get()) + print(config["foo"]["bar"].get()) .. _argparse: http://docs.python.org/dev/library/argparse.html -.. _parse_args: http://docs.python.org/library/argparse.html#the-parse-args-method + .. _optparse: http://docs.python.org/library/optparse.html -Note that, while you can use the full power of your favorite -command-line parsing library, you'll probably want to avoid specifying -defaults in your argparse or optparse setup. This way, Confuse can use -other configuration sources---possibly your -``config_default.yaml``---to fill in values for unspecified -command-line switches. Otherwise, the argparse/optparse default value -will hide options configured elsewhere. +.. _parse_args: http://docs.python.org/library/argparse.html#the-parse-args-method +Note that, while you can use the full power of your favorite command-line +parsing library, you'll probably want to avoid specifying defaults in your +argparse or optparse setup. This way, Confuse can use other configuration +sources---possibly your ``config_default.yaml``---to fill in values for +unspecified command-line switches. Otherwise, the argparse/optparse default +value will hide options configured elsewhere. Environment Variables --------------------- @@ -238,12 +218,12 @@ Environment Variables Confuse supports using environment variables as another source to provide an additional layer of configuration. The environment variables to include are identified by a prefix, which defaults to the uppercased name of your -application followed by an underscore. Matching environment variable names -are first stripped of this prefix and then lowercased to determine the -corresponding configuration option. To load the environment variables for -your application using the default prefix, just call ``set_env`` on your -``Configuration`` object. Config values from the environment will then be -added as an overlay at the highest precedence. For example: +application followed by an underscore. Matching environment variable names are +first stripped of this prefix and then lowercased to determine the corresponding +configuration option. To load the environment variables for your application +using the default prefix, just call ``set_env`` on your ``Configuration`` +object. Config values from the environment will then be added as an overlay at +the highest precedence. For example: .. code-block:: sh @@ -252,15 +232,16 @@ added as an overlay at the highest precedence. For example: .. code-block:: python import confuse - config = confuse.Configuration('myapp', __name__) + + config = confuse.Configuration("myapp", __name__) config.set_env() - print(config['foo'].get()) + print(config["foo"].get()) Nested config values can be overridden by using a separator string in the environment variable name. By default, double underscores are used as the -separator for nesting, to avoid clashes with config options that contain -single underscores. Note that most shells restrict environment variable names -to alphanumeric and underscore characters, so dots are not a valid separator. +separator for nesting, to avoid clashes with config options that contain single +underscores. Note that most shells restrict environment variable names to +alphanumeric and underscore characters, so dots are not a valid separator. .. code-block:: sh @@ -269,12 +250,13 @@ to alphanumeric and underscore characters, so dots are not a valid separator. .. code-block:: python import confuse - config = confuse.Configuration('myapp', __name__) + + config = confuse.Configuration("myapp", __name__) config.set_env() - print(config['foo']['bar'].get()) + print(config["foo"]["bar"].get()) -Both the prefix and the separator can be customized when using ``set_env``. -Note that prefix matching is done to the environment variables *prior* to +Both the prefix and the separator can be customized when using ``set_env``. Note +that prefix matching is done to the environment variables *prior* to lowercasing, while the separator is matched *after* lowercasing. .. code-block:: sh @@ -284,9 +266,10 @@ lowercasing, while the separator is matched *after* lowercasing. .. code-block:: python import confuse - config = confuse.Configuration('myapp', __name__) - config.set_env(prefix='APP', sep='_nested_') - print(config['foo']['bar'].get()) + + config = confuse.Configuration("myapp", __name__) + config.set_env(prefix="APP", sep="_nested_") + print(config["foo"]["bar"].get()) For configurations that include lists, use integers starting from 0 as nested keys to invoke "list conversion." If any of the sibling nested keys are not @@ -299,167 +282,157 @@ are supported. export MYAPP_FOO__0=first export MYAPP_FOO__1=second export MYAPP_FOO__2__BAR__0=nested - + .. code-block:: python import confuse - config = confuse.Configuration('myapp', __name__) + + config = confuse.Configuration("myapp", __name__) config.set_env() - print(config['foo'].get()) # ['first', 'second', {'bar': ['nested']}] + print(config["foo"].get()) # ['first', 'second', {'bar': ['nested']}] -For consistency with YAML config files, the values of environment variables -are type converted using the same YAML parser used for file-based configs. -This means that numeric strings will be converted to integers or floats, "true" -and "false" will be converted to booleans, and the empty string or "null" will -be converted to ``None``. Setting an environment variable to the empty string -or "null" allows unsetting a config value from a lower-precedence source. +For consistency with YAML config files, the values of environment variables are +type converted using the same YAML parser used for file-based configs. This +means that numeric strings will be converted to integers or floats, "true" and +"false" will be converted to booleans, and the empty string or "null" will be +converted to ``None``. Setting an environment variable to the empty string or +"null" allows unsetting a config value from a lower-precedence source. To change the lowercasing and list handling behaviors when loading environment variables or to enable full YAML parsing of environment variables, you can initialize an ``EnvSource`` configuration source directly. If you use config overlays from both command-line args and environment -variables, the order of calls to ``set_args`` and ``set_env`` will -determine the precedence, with the last call having the highest precedence. - +variables, the order of calls to ``set_args`` and ``set_env`` will determine the +precedence, with the last call having the highest precedence. Search Paths ------------ -Confuse looks in a number of locations for your application's -configurations. The locations are determined by the platform. For each -platform, Confuse has a list of directories in which it looks for a -directory named after the application. For example, the first search -location on Unix-y systems is ``$XDG_CONFIG_HOME/AppName`` for an -application called ``AppName``. +Confuse looks in a number of locations for your application's configurations. +The locations are determined by the platform. For each platform, Confuse has a +list of directories in which it looks for a directory named after the +application. For example, the first search location on Unix-y systems is +``$XDG_CONFIG_HOME/AppName`` for an application called ``AppName``. Here are the default search paths for each platform: -* macOS: ``~/.config/app`` and ``~/Library/Application Support/app`` -* Other Unix: ``~/.config/app`` and ``/etc/app`` -* Windows: ``%APPDATA%\app`` where the `APPDATA` environment variable falls - back to ``%HOME%\AppData\Roaming`` if undefined +- macOS: ``~/.config/app`` and ``~/Library/Application Support/app`` +- Other Unix: ``~/.config/app`` and ``/etc/app`` +- Windows: ``%APPDATA%\app`` where the `APPDATA` environment variable falls back + to ``%HOME%\AppData\Roaming`` if undefined Both macOS and other Unix operating sytems also try to use the -``XDG_CONFIG_HOME`` and ``XDG_CONFIG_DIRS`` environment variables if set -then search those directories as well. - -Users can also add an override configuration directory with an -environment variable. The variable name is the application name in -capitals with "DIR" appended: for an application named ``AppName``, the -environment variable is ``APPNAMEDIR``. +``XDG_CONFIG_HOME`` and ``XDG_CONFIG_DIRS`` environment variables if set then +search those directories as well. +Users can also add an override configuration directory with an environment +variable. The variable name is the application name in capitals with "DIR" +appended: for an application named ``AppName``, the environment variable is +``APPNAMEDIR``. Manually Specifying Config Files -------------------------------- -You may want to leverage Confuse's features without :ref:`Search Paths`. -This can be done by manually specifying the YAML files you want to include, -which also allows changing how relative paths in the file will be resolved: +You may want to leverage Confuse's features without :ref:`Search Paths`. This +can be done by manually specifying the YAML files you want to include, which +also allows changing how relative paths in the file will be resolved: .. code-block:: python import confuse + # Instantiates config. Confuse searches for a config_default.yaml - config = confuse.Configuration('MyGreatApp', __name__) + config = confuse.Configuration("MyGreatApp", __name__) # Add config items from specified file. Relative path values within the # file are resolved relative to the application's configuration directory. - config.set_file('subdirectory/default_config.yaml') + config.set_file("subdirectory/default_config.yaml") # Add config items from a second file. If some items were already defined, # they will be overwritten (new file precedes the previous ones). With # `base_for_paths` set to True, relative path values in this file will be # resolved relative to the config file's directory (i.e., 'subdirectory'). - config.set_file('subdirectory/local_config.yaml', base_for_paths=True) - - val = config['foo']['bar'].get(int) + config.set_file("subdirectory/local_config.yaml", base_for_paths=True) + val = config["foo"]["bar"].get(int) Your Application Directory -------------------------- -Confuse provides a simple helper, ``Configuration.config_dir()``, that -gives you a directory used to store your application's configuration. If -a configuration file exists in any of the searched locations, then the -highest-priority directory containing a config file is used. Otherwise, -a directory is created for you and returned. So you can always expect -this method to give you a directory that actually exists. +Confuse provides a simple helper, ``Configuration.config_dir()``, that gives you +a directory used to store your application's configuration. If a configuration +file exists in any of the searched locations, then the highest-priority +directory containing a config file is used. Otherwise, a directory is created +for you and returned. So you can always expect this method to give you a +directory that actually exists. -As an example, you may want to migrate a user's settings to Confuse from -an older configuration system such as `ConfigParser`_. Just do something -like this: +As an example, you may want to migrate a user's settings to Confuse from an +older configuration system such as ConfigParser_. Just do something like this: .. code-block:: python - config_filename = os.path.join(config.config_dir(), - confuse.CONFIG_FILENAME) - with open(config_filename, 'w') as f: + config_filename = os.path.join(config.config_dir(), confuse.CONFIG_FILENAME) + with open(config_filename, "w") as f: yaml.dump(migrated_config, f) -.. _ConfigParser: http://docs.python.org/library/configparser.html - +.. _configparser: http://docs.python.org/library/configparser.html Dynamic Updates --------------- Occasionally, a program will need to modify its configuration while it's -running. For example, an interactive prompt from the user might cause -the program to change a setting for the current execution only. Or the -program might need to add a *derived* configuration value that the user -doesn't specify. +running. For example, an interactive prompt from the user might cause the +program to change a setting for the current execution only. Or the program might +need to add a *derived* configuration value that the user doesn't specify. -To facilitate this, Confuse lets you *assign* to view objects using -ordinary Python assignment. Assignment will add an overlay source that -precedes all other configuration sources in priority. Here's an example -of programmatically setting a configuration value based on a ``DEBUG`` -constant: +To facilitate this, Confuse lets you *assign* to view objects using ordinary +Python assignment. Assignment will add an overlay source that precedes all other +configuration sources in priority. Here's an example of programmatically setting +a configuration value based on a ``DEBUG`` constant: .. code-block:: python if DEBUG: - config['verbosity'] = 100 + config["verbosity"] = 100 ... - my_logger.setLevel(config['verbosity'].get(int)) + my_logger.setLevel(config["verbosity"].get(int)) -This example allows the constant to override the default verbosity -level, which would otherwise come from a configuration file. - -Assignment works by creating a new "source" for configuration data at -the top of the stack. This new source takes priority over all other, -previously-loaded sources. You can cause this explicitly by calling the -``set()`` method on any view. A related method, ``add()``, works -similarly but instead adds a new *lowest-priority* source to the bottom -of the stack. This can be used to provide defaults for options that may -be overridden by previously-loaded configuration files. +This example allows the constant to override the default verbosity level, which +would otherwise come from a configuration file. +Assignment works by creating a new "source" for configuration data at the top of +the stack. This new source takes priority over all other, previously-loaded +sources. You can cause this explicitly by calling the ``set()`` method on any +view. A related method, ``add()``, works similarly but instead adds a new +*lowest-priority* source to the bottom of the stack. This can be used to provide +defaults for options that may be overridden by previously-loaded configuration +files. YAML Tweaks ----------- -Confuse uses the `PyYAML`_ module to parse YAML configuration files. However, it +Confuse uses the PyYAML_ module to parse YAML configuration files. However, it deviates very slightly from the official YAML specification to provide a few niceties suited to human-written configuration files. Those tweaks are: .. _pyyaml: http://pyyaml.org/ - All strings are returned as Python Unicode objects. -- YAML maps are parsed as Python `OrderedDict`_ objects. This means that you - can recover the order that the user wrote down a dictionary. +- YAML maps are parsed as Python OrderedDict_ objects. This means that you can + recover the order that the user wrote down a dictionary. - Bare strings can begin with the % character. In stock PyYAML, this will throw a parse error. -.. _OrderedDict: http://docs.python.org/2/library/collections.html#collections.OrderedDict +.. _ordereddict: http://docs.python.org/2/library/collections.html#collections.OrderedDict To produce a YAML string reflecting a configuration, just call -``config.dump()``. This does not cleanly round-trip YAML, -but it does play some tricks to preserve comments and spacing in the original -file. +``config.dump()``. This does not cleanly round-trip YAML, but it does play some +tricks to preserve comments and spacing in the original file. Custom YAML Loaders -''''''''''''''''''' +~~~~~~~~~~~~~~~~~~~ -You can also specify your own `PyYAML`_ `Loader` object to parse YAML -files. Supply the `loader` parameter to a `Configuration` constructor, -like this: +You can also specify your own PyYAML_ `Loader` object to parse YAML files. +Supply the `loader` parameter to a `Configuration` constructor, like this: .. code-block:: python @@ -470,58 +443,56 @@ To imbue a loader with Confuse's special parser overrides, use its .. code-block:: python - class MyLoader(yaml.Loader): - ... + class MyLoader(yaml.Loader): ... + + confuse.Loader.add_constructors(MyLoader) config = confuse.Configuration("name", loader=MyLoader) - Configuring Large Programs -------------------------- -One problem that must be solved by a configuration system is the issue -of global configuration for complex applications. In a large program -with many components and many config options, it can be unwieldy to -explicitly pass configuration values from component to component. You -quickly end up with monstrous function signatures with dozens of keyword -arguments, decreasing code legibility and testability. - -In such systems, one option is to pass a single `Configuration` object -through to each component. To avoid even this, however, it's sometimes -appropriate to use a little bit of shared global state. As evil as -shared global state usually is, configuration is (in my opinion) one -valid use: since configuration is mostly read-only, it's relatively -unlikely to cause the sorts of problems that global values sometimes -can. And having a global repository for configuration option can vastly -reduce the amount of boilerplate threading-through needed to explicitly -pass configuration from call to call. - -To use global configuration, consider creating a configuration object in -a well-known module (say, the root of a package). But since this object -will be initialized at module load time, Confuse provides a `LazyConfig` -object that loads your configuration files on demand instead of when the -object is constructed. (Doing complicated stuff like parsing YAML at -module load time is generally considered a Bad Idea.) - -Global state can cause problems for unit testing. To alleviate this, -consider adding code to your test fixtures (e.g., `setUp`_ in the -`unittest`_ module) that clears out the global configuration before each -test is run. Something like this: +One problem that must be solved by a configuration system is the issue of global +configuration for complex applications. In a large program with many components +and many config options, it can be unwieldy to explicitly pass configuration +values from component to component. You quickly end up with monstrous function +signatures with dozens of keyword arguments, decreasing code legibility and +testability. + +In such systems, one option is to pass a single `Configuration` object through +to each component. To avoid even this, however, it's sometimes appropriate to +use a little bit of shared global state. As evil as shared global state usually +is, configuration is (in my opinion) one valid use: since configuration is +mostly read-only, it's relatively unlikely to cause the sorts of problems that +global values sometimes can. And having a global repository for configuration +option can vastly reduce the amount of boilerplate threading-through needed to +explicitly pass configuration from call to call. + +To use global configuration, consider creating a configuration object in a +well-known module (say, the root of a package). But since this object will be +initialized at module load time, Confuse provides a `LazyConfig` object that +loads your configuration files on demand instead of when the object is +constructed. (Doing complicated stuff like parsing YAML at module load time is +generally considered a Bad Idea.) + +Global state can cause problems for unit testing. To alleviate this, consider +adding code to your test fixtures (e.g., setUp_ in the unittest_ module) that +clears out the global configuration before each test is run. Something like +this: .. code-block:: python config.clear() config.read(user=False) -These lines will empty out the current configuration and then re-load -the defaults (but not the user's configuration files). Your tests can -then modify the global configuration values without affecting other -tests since these modifications will be cleared out before the next test -runs. +These lines will empty out the current configuration and then re-load the +defaults (but not the user's configuration files). Your tests can then modify +the global configuration values without affecting other tests since these +modifications will be cleared out before the next test runs. -.. _unittest: http://docs.python.org/2/library/unittest.html -.. _setUp: http://docs.python.org/2/library/unittest.html#unittest.TestCase.setUp +.. _setup: http://docs.python.org/2/library/unittest.html#unittest.TestCase.setUp +.. _unittest: http://docs.python.org/2/library/unittest.html Redaction --------- @@ -531,7 +502,7 @@ including them in output. Just set the `redact` flag: .. code-block:: python - config['key'].redact = True + config["key"].redact = True Then flatten or dump the configuration like so: From 8aa6d237ac6479971ffe38608b2526ef0abe55f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= Date: Sat, 27 Dec 2025 17:42:43 +0000 Subject: [PATCH 06/15] Update URLs in the docs --- README.rst | 18 +++++++++--------- docs/usage.rst | 20 ++++++++++---------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/README.rst b/README.rst index c13f361..04bb832 100644 --- a/README.rst +++ b/README.rst @@ -4,8 +4,8 @@ Confuse: painless YAML config files .. image:: https://github.com/beetbox/confuse/actions/workflows/main.yml/badge.svg :target: https://github.com/beetbox/confuse/actions -.. image:: http://img.shields.io/pypi/v/confuse.svg - :target: https://pypi.python.org/pypi/confuse +.. image:: https://img.shields.io/pypi/v/confuse.svg + :target: https://pypi.org/project/confuse/ **Confuse** is a configuration library for Python that uses YAML_. It takes care of defaults, overrides, type checking, command-line integration, environment @@ -60,18 +60,18 @@ Credits Confuse was made to power beets_. Like beets, it is available under the `MIT license`_. -.. _argparse: http://docs.python.org/dev/library/argparse.html +.. _argparse: https://docs.python.org/dev/library/argparse.html .. _beets: https://github.com/beetbox/beets -.. _configparser: http://docs.python.org/library/configparser.html +.. _configparser: https://docs.python.org/library/configparser.html -.. _confuse's documentation: http://confuse.readthedocs.org/en/latest/usage.html +.. _confuse's documentation: https://confuse.readthedocs.io/en/latest/usage.html -.. _logging: http://docs.python.org/library/logging.html +.. _logging: https://docs.python.org/library/logging.html -.. _mit license: http://www.opensource.org/licenses/mit-license.php +.. _mit license: https://opensource.org/license/mit -.. _optparse: http://docs.python.org/dev/library/optparse.html +.. _optparse: https://docs.python.org/dev/library/optparse.html -.. _yaml: http://yaml.org/ +.. _yaml: https://yaml.org/ diff --git a/docs/usage.rst b/docs/usage.rst index 4aa963d..e707d2d 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -97,7 +97,7 @@ granularity you want config files to override each other. an empty configuration at the top of the stack. A subsequent call to ``config.get()`` might then return no configuration at all. -.. _xpath: http://www.w3.org/TR/xpath/ +.. _xpath: https://www.w3.org/TR/xpath/ Validation ---------- @@ -131,7 +131,7 @@ that perform fancier validation or even conversion: - ``as_str_expanded()``: Expand any environment variables contained in a string using `os.path.expandvars()`_. -.. _os.path.expandvars(): https://docs.python.org/library/os.path.html#os.path.expandvars +.. _os.path.expandvars(): https://docs.python.org/3/library/os.path.html#os.path.expandvars For example, ``config['path'].as_filename()`` ensures that you get a reasonable filename string from the configuration. And calling @@ -199,11 +199,11 @@ dot-delimited properties on the incoming object. config.set_args(args, dots=True) print(config["foo"]["bar"].get()) -.. _argparse: http://docs.python.org/dev/library/argparse.html +.. _argparse: https://docs.python.org/dev/library/argparse.html -.. _optparse: http://docs.python.org/library/optparse.html +.. _optparse: https://docs.python.org/3/library/optparse.html -.. _parse_args: http://docs.python.org/library/argparse.html#the-parse-args-method +.. _parse_args: https://docs.python.org/library/argparse.html#the-parse-args-method Note that, while you can use the full power of your favorite command-line parsing library, you'll probably want to avoid specifying defaults in your @@ -374,7 +374,7 @@ older configuration system such as ConfigParser_. Just do something like this: with open(config_filename, "w") as f: yaml.dump(migrated_config, f) -.. _configparser: http://docs.python.org/library/configparser.html +.. _configparser: https://docs.python.org/3/library/configparser.html Dynamic Updates --------------- @@ -414,7 +414,7 @@ Confuse uses the PyYAML_ module to parse YAML configuration files. However, it deviates very slightly from the official YAML specification to provide a few niceties suited to human-written configuration files. Those tweaks are: -.. _pyyaml: http://pyyaml.org/ +.. _pyyaml: https://pyyaml.org/ - All strings are returned as Python Unicode objects. - YAML maps are parsed as Python OrderedDict_ objects. This means that you can @@ -422,7 +422,7 @@ niceties suited to human-written configuration files. Those tweaks are: - Bare strings can begin with the % character. In stock PyYAML, this will throw a parse error. -.. _ordereddict: http://docs.python.org/2/library/collections.html#collections.OrderedDict +.. _ordereddict: https://docs.python.org/2/library/collections.html#collections.OrderedDict To produce a YAML string reflecting a configuration, just call ``config.dump()``. This does not cleanly round-trip YAML, but it does play some @@ -490,9 +490,9 @@ defaults (but not the user's configuration files). Your tests can then modify the global configuration values without affecting other tests since these modifications will be cleared out before the next test runs. -.. _setup: http://docs.python.org/2/library/unittest.html#unittest.TestCase.setUp +.. _setup: https://docs.python.org/2/library/unittest.html#unittest.TestCase.setUp -.. _unittest: http://docs.python.org/2/library/unittest.html +.. _unittest: https://docs.python.org/2/library/unittest.html Redaction --------- From afd36c617068526139cb93aa298c530977e4a934 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= Date: Sat, 27 Dec 2025 18:25:55 +0000 Subject: [PATCH 07/15] Fix python-version ci config --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7556a2f..d0a1a09 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] steps: - uses: actions/checkout@v4 - name: Install Python tools From cdb4c79290de4cb725457bc35523b4d9fc487806 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= Date: Sat, 27 Dec 2025 18:26:22 +0000 Subject: [PATCH 08/15] Enable tests on windows --- .github/workflows/main.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d0a1a09..306127e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -6,10 +6,12 @@ on: [push] jobs: test: name: Run tests - runs-on: ubuntu-latest strategy: + fail-fast: false matrix: + platform: [ubuntu-latest, windows-latest] python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] + runs-on: ${{ matrix.platform }} steps: - uses: actions/checkout@v4 - name: Install Python tools From 6975363f373b7064827d02331c9f2e178fb70059 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= Date: Mon, 29 Dec 2025 01:39:23 +0000 Subject: [PATCH 09/15] Drop support for Python 3.9, re-enable docstrfmt --- .github/workflows/lint.yaml | 5 +- .github/workflows/main.yml | 2 +- .github/workflows/make_release.yaml | 2 +- docs/changelog.rst | 5 + poetry.lock | 448 +++++++++------------------- pyproject.toml | 10 +- 6 files changed, 153 insertions(+), 319 deletions(-) diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 8452464..a4169b4 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -136,9 +136,8 @@ jobs: echo "::add-matcher::.github/problem-matchers/sphinx-build.json" echo "::add-matcher::.github/problem-matchers/sphinx-lint.json" - # TODO: Re-enable once https://github.com/LilSpazJoekp/docstrfmt/pull/169 is merged and released - # - name: Check docs formatting - # run: poe format-docs --check + - name: Check docs formatting + run: poe format-docs --check - name: Lint docs run: poe lint-docs diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 306127e..2ac9395 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -10,7 +10,7 @@ jobs: fail-fast: false matrix: platform: [ubuntu-latest, windows-latest] - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] runs-on: ${{ matrix.platform }} steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/make_release.yaml b/.github/workflows/make_release.yaml index de02836..b2e2b3c 100644 --- a/.github/workflows/make_release.yaml +++ b/.github/workflows/make_release.yaml @@ -8,7 +8,7 @@ on: required: true env: - PYTHON_VERSION: 3.9 + PYTHON_VERSION: 3.10 NEW_VERSION: ${{ inputs.version }} NEW_TAG: v${{ inputs.version }} diff --git a/docs/changelog.rst b/docs/changelog.rst index 6ac33d0..d2266d7 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,11 @@ Changelog ========= +Unreleased +---------- + +- Drop support for Python 3.9. + v2.1.1 ------ diff --git a/poetry.lock b/poetry.lock index edf04f7..669e418 100644 --- a/poetry.lock +++ b/poetry.lock @@ -29,59 +29,6 @@ markers = {main = "extra == \"docs\""} [package.extras] dev = ["backports.zoneinfo ; python_version < \"3.9\"", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest (>=6.0)", "pytest-cov", "pytz", "setuptools", "tzdata ; sys_platform == \"win32\""] -[[package]] -name = "black" -version = "25.11.0" -description = "The uncompromising code formatter." -optional = false -python-versions = ">=3.9" -groups = ["lint"] -markers = "python_version < \"3.13\"" -files = [ - {file = "black-25.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ec311e22458eec32a807f029b2646f661e6859c3f61bc6d9ffb67958779f392e"}, - {file = "black-25.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1032639c90208c15711334d681de2e24821af0575573db2810b0763bcd62e0f0"}, - {file = "black-25.11.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0c0f7c461df55cf32929b002335883946a4893d759f2df343389c4396f3b6b37"}, - {file = "black-25.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:f9786c24d8e9bd5f20dc7a7f0cdd742644656987f6ea6947629306f937726c03"}, - {file = "black-25.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:895571922a35434a9d8ca67ef926da6bc9ad464522a5fe0db99b394ef1c0675a"}, - {file = "black-25.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cb4f4b65d717062191bdec8e4a442539a8ea065e6af1c4f4d36f0cdb5f71e170"}, - {file = "black-25.11.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d81a44cbc7e4f73a9d6ae449ec2317ad81512d1e7dce7d57f6333fd6259737bc"}, - {file = "black-25.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:7eebd4744dfe92ef1ee349dc532defbf012a88b087bb7ddd688ff59a447b080e"}, - {file = "black-25.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:80e7486ad3535636657aa180ad32a7d67d7c273a80e12f1b4bfa0823d54e8fac"}, - {file = "black-25.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6cced12b747c4c76bc09b4db057c319d8545307266f41aaee665540bc0e04e96"}, - {file = "black-25.11.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cb2d54a39e0ef021d6c5eef442e10fd71fcb491be6413d083a320ee768329dd"}, - {file = "black-25.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:ae263af2f496940438e5be1a0c1020e13b09154f3af4df0835ea7f9fe7bfa409"}, - {file = "black-25.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0a1d40348b6621cc20d3d7530a5b8d67e9714906dfd7346338249ad9c6cedf2b"}, - {file = "black-25.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:51c65d7d60bb25429ea2bf0731c32b2a2442eb4bd3b2afcb47830f0b13e58bfd"}, - {file = "black-25.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:936c4dd07669269f40b497440159a221ee435e3fddcf668e0c05244a9be71993"}, - {file = "black-25.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:f42c0ea7f59994490f4dccd64e6b2dd49ac57c7c84f38b8faab50f8759db245c"}, - {file = "black-25.11.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:35690a383f22dd3e468c85dc4b915217f87667ad9cce781d7b42678ce63c4170"}, - {file = "black-25.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:dae49ef7369c6caa1a1833fd5efb7c3024bb7e4499bf64833f65ad27791b1545"}, - {file = "black-25.11.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bd4a22a0b37401c8e492e994bce79e614f91b14d9ea911f44f36e262195fdda"}, - {file = "black-25.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:aa211411e94fdf86519996b7f5f05e71ba34835d8f0c0f03c00a26271da02664"}, - {file = "black-25.11.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a3bb5ce32daa9ff0605d73b6f19da0b0e6c1f8f2d75594db539fdfed722f2b06"}, - {file = "black-25.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9815ccee1e55717fe9a4b924cae1646ef7f54e0f990da39a34fc7b264fcf80a2"}, - {file = "black-25.11.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:92285c37b93a1698dcbc34581867b480f1ba3a7b92acf1fe0467b04d7a4da0dc"}, - {file = "black-25.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:43945853a31099c7c0ff8dface53b4de56c41294fa6783c0441a8b1d9bf668bc"}, - {file = "black-25.11.0-py3-none-any.whl", hash = "sha256:e3f562da087791e96cefcd9dda058380a442ab322a02e222add53736451f604b"}, - {file = "black-25.11.0.tar.gz", hash = "sha256:9a323ac32f5dc75ce7470501b887250be5005a01602e931a15e45593f70f6e08"}, -] - -[package.dependencies] -click = ">=8.0.0" -mypy-extensions = ">=0.4.3" -packaging = ">=22.0" -pathspec = ">=0.9.0" -platformdirs = ">=2" -pytokens = ">=0.3.0" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} - -[package.extras] -colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.10)"] -jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -uvloop = ["uvloop (>=0.15.2)"] - [[package]] name = "black" version = "25.12.0" @@ -89,7 +36,6 @@ description = "The uncompromising code formatter." optional = false python-versions = ">=3.10" groups = ["lint"] -markers = "python_version >= \"3.13\"" files = [ {file = "black-25.12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f85ba1ad15d446756b4ab5f3044731bf68b777f8f9ac9cdabd2425b97cd9c4e8"}, {file = "black-25.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:546eecfe9a3a6b46f9d69d8a642585a6eaf348bcbbc4d87a19635570e02d9f4a"}, @@ -127,6 +73,8 @@ packaging = ">=22.0" pathspec = ">=0.9.0" platformdirs = ">=2" pytokens = ">=0.3.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} [package.extras] colorama = ["colorama (>=0.4.3)"] @@ -271,22 +219,6 @@ files = [ ] markers = {main = "extra == \"docs\""} -[[package]] -name = "click" -version = "8.1.8" -description = "Composable command line interface toolkit" -optional = false -python-versions = ">=3.7" -groups = ["lint"] -markers = "python_version < \"3.13\"" -files = [ - {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, - {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - [[package]] name = "click" version = "8.3.1" @@ -294,7 +226,6 @@ description = "Composable command line interface toolkit" optional = false python-versions = ">=3.10" groups = ["lint"] -markers = "python_version >= \"3.13\"" files = [ {file = "click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6"}, {file = "click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a"}, @@ -318,116 +249,104 @@ markers = {main = "extra == \"docs\" and sys_platform == \"win32\"", dev = "sys_ [[package]] name = "coverage" -version = "7.10.7" +version = "7.13.0" description = "Code coverage measurement for Python" optional = false -python-versions = ">=3.9" -groups = ["dev"] +python-versions = ">=3.10" +groups = ["dev", "lint"] files = [ - {file = "coverage-7.10.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fc04cc7a3db33664e0c2d10eb8990ff6b3536f6842c9590ae8da4c614b9ed05a"}, - {file = "coverage-7.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e201e015644e207139f7e2351980feb7040e6f4b2c2978892f3e3789d1c125e5"}, - {file = "coverage-7.10.7-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:240af60539987ced2c399809bd34f7c78e8abe0736af91c3d7d0e795df633d17"}, - {file = "coverage-7.10.7-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8421e088bc051361b01c4b3a50fd39a4b9133079a2229978d9d30511fd05231b"}, - {file = "coverage-7.10.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6be8ed3039ae7f7ac5ce058c308484787c86e8437e72b30bf5e88b8ea10f3c87"}, - {file = "coverage-7.10.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e28299d9f2e889e6d51b1f043f58d5f997c373cc12e6403b90df95b8b047c13e"}, - {file = "coverage-7.10.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c4e16bd7761c5e454f4efd36f345286d6f7c5fa111623c355691e2755cae3b9e"}, - {file = "coverage-7.10.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b1c81d0e5e160651879755c9c675b974276f135558cf4ba79fee7b8413a515df"}, - {file = "coverage-7.10.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:606cc265adc9aaedcc84f1f064f0e8736bc45814f15a357e30fca7ecc01504e0"}, - {file = "coverage-7.10.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:10b24412692df990dbc34f8fb1b6b13d236ace9dfdd68df5b28c2e39cafbba13"}, - {file = "coverage-7.10.7-cp310-cp310-win32.whl", hash = "sha256:b51dcd060f18c19290d9b8a9dd1e0181538df2ce0717f562fff6cf74d9fc0b5b"}, - {file = "coverage-7.10.7-cp310-cp310-win_amd64.whl", hash = "sha256:3a622ac801b17198020f09af3eaf45666b344a0d69fc2a6ffe2ea83aeef1d807"}, - {file = "coverage-7.10.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a609f9c93113be646f44c2a0256d6ea375ad047005d7f57a5c15f614dc1b2f59"}, - {file = "coverage-7.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:65646bb0359386e07639c367a22cf9b5bf6304e8630b565d0626e2bdf329227a"}, - {file = "coverage-7.10.7-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5f33166f0dfcce728191f520bd2692914ec70fac2713f6bf3ce59c3deacb4699"}, - {file = "coverage-7.10.7-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:35f5e3f9e455bb17831876048355dca0f758b6df22f49258cb5a91da23ef437d"}, - {file = "coverage-7.10.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4da86b6d62a496e908ac2898243920c7992499c1712ff7c2b6d837cc69d9467e"}, - {file = "coverage-7.10.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6b8b09c1fad947c84bbbc95eca841350fad9cbfa5a2d7ca88ac9f8d836c92e23"}, - {file = "coverage-7.10.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4376538f36b533b46f8971d3a3e63464f2c7905c9800db97361c43a2b14792ab"}, - {file = "coverage-7.10.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:121da30abb574f6ce6ae09840dae322bef734480ceafe410117627aa54f76d82"}, - {file = "coverage-7.10.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:88127d40df529336a9836870436fc2751c339fbaed3a836d42c93f3e4bd1d0a2"}, - {file = "coverage-7.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ba58bbcd1b72f136080c0bccc2400d66cc6115f3f906c499013d065ac33a4b61"}, - {file = "coverage-7.10.7-cp311-cp311-win32.whl", hash = "sha256:972b9e3a4094b053a4e46832b4bc829fc8a8d347160eb39d03f1690316a99c14"}, - {file = "coverage-7.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:a7b55a944a7f43892e28ad4bc0561dfd5f0d73e605d1aa5c3c976b52aea121d2"}, - {file = "coverage-7.10.7-cp311-cp311-win_arm64.whl", hash = "sha256:736f227fb490f03c6488f9b6d45855f8e0fd749c007f9303ad30efab0e73c05a"}, - {file = "coverage-7.10.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7bb3b9ddb87ef7725056572368040c32775036472d5a033679d1fa6c8dc08417"}, - {file = "coverage-7.10.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:18afb24843cbc175687225cab1138c95d262337f5473512010e46831aa0c2973"}, - {file = "coverage-7.10.7-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:399a0b6347bcd3822be369392932884b8216d0944049ae22925631a9b3d4ba4c"}, - {file = "coverage-7.10.7-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314f2c326ded3f4b09be11bc282eb2fc861184bc95748ae67b360ac962770be7"}, - {file = "coverage-7.10.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c41e71c9cfb854789dee6fc51e46743a6d138b1803fab6cb860af43265b42ea6"}, - {file = "coverage-7.10.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc01f57ca26269c2c706e838f6422e2a8788e41b3e3c65e2f41148212e57cd59"}, - {file = "coverage-7.10.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a6442c59a8ac8b85812ce33bc4d05bde3fb22321fa8294e2a5b487c3505f611b"}, - {file = "coverage-7.10.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:78a384e49f46b80fb4c901d52d92abe098e78768ed829c673fbb53c498bef73a"}, - {file = "coverage-7.10.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5e1e9802121405ede4b0133aa4340ad8186a1d2526de5b7c3eca519db7bb89fb"}, - {file = "coverage-7.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d41213ea25a86f69efd1575073d34ea11aabe075604ddf3d148ecfec9e1e96a1"}, - {file = "coverage-7.10.7-cp312-cp312-win32.whl", hash = "sha256:77eb4c747061a6af8d0f7bdb31f1e108d172762ef579166ec84542f711d90256"}, - {file = "coverage-7.10.7-cp312-cp312-win_amd64.whl", hash = "sha256:f51328ffe987aecf6d09f3cd9d979face89a617eacdaea43e7b3080777f647ba"}, - {file = "coverage-7.10.7-cp312-cp312-win_arm64.whl", hash = "sha256:bda5e34f8a75721c96085903c6f2197dc398c20ffd98df33f866a9c8fd95f4bf"}, - {file = "coverage-7.10.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:981a651f543f2854abd3b5fcb3263aac581b18209be49863ba575de6edf4c14d"}, - {file = "coverage-7.10.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73ab1601f84dc804f7812dc297e93cd99381162da39c47040a827d4e8dafe63b"}, - {file = "coverage-7.10.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a8b6f03672aa6734e700bbcd65ff050fd19cddfec4b031cc8cf1c6967de5a68e"}, - {file = "coverage-7.10.7-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10b6ba00ab1132a0ce4428ff68cf50a25efd6840a42cdf4239c9b99aad83be8b"}, - {file = "coverage-7.10.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c79124f70465a150e89340de5963f936ee97097d2ef76c869708c4248c63ca49"}, - {file = "coverage-7.10.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:69212fbccdbd5b0e39eac4067e20a4a5256609e209547d86f740d68ad4f04911"}, - {file = "coverage-7.10.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7ea7c6c9d0d286d04ed3541747e6597cbe4971f22648b68248f7ddcd329207f0"}, - {file = "coverage-7.10.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b9be91986841a75042b3e3243d0b3cb0b2434252b977baaf0cd56e960fe1e46f"}, - {file = "coverage-7.10.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b281d5eca50189325cfe1f365fafade89b14b4a78d9b40b05ddd1fc7d2a10a9c"}, - {file = "coverage-7.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:99e4aa63097ab1118e75a848a28e40d68b08a5e19ce587891ab7fd04475e780f"}, - {file = "coverage-7.10.7-cp313-cp313-win32.whl", hash = "sha256:dc7c389dce432500273eaf48f410b37886be9208b2dd5710aaf7c57fd442c698"}, - {file = "coverage-7.10.7-cp313-cp313-win_amd64.whl", hash = "sha256:cac0fdca17b036af3881a9d2729a850b76553f3f716ccb0360ad4dbc06b3b843"}, - {file = "coverage-7.10.7-cp313-cp313-win_arm64.whl", hash = "sha256:4b6f236edf6e2f9ae8fcd1332da4e791c1b6ba0dc16a2dc94590ceccb482e546"}, - {file = "coverage-7.10.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a0ec07fd264d0745ee396b666d47cef20875f4ff2375d7c4f58235886cc1ef0c"}, - {file = "coverage-7.10.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd5e856ebb7bfb7672b0086846db5afb4567a7b9714b8a0ebafd211ec7ce6a15"}, - {file = "coverage-7.10.7-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f57b2a3c8353d3e04acf75b3fed57ba41f5c0646bbf1d10c7c282291c97936b4"}, - {file = "coverage-7.10.7-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ef2319dd15a0b009667301a3f84452a4dc6fddfd06b0c5c53ea472d3989fbf0"}, - {file = "coverage-7.10.7-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83082a57783239717ceb0ad584de3c69cf581b2a95ed6bf81ea66034f00401c0"}, - {file = "coverage-7.10.7-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:50aa94fb1fb9a397eaa19c0d5ec15a5edd03a47bf1a3a6111a16b36e190cff65"}, - {file = "coverage-7.10.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2120043f147bebb41c85b97ac45dd173595ff14f2a584f2963891cbcc3091541"}, - {file = "coverage-7.10.7-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2fafd773231dd0378fdba66d339f84904a8e57a262f583530f4f156ab83863e6"}, - {file = "coverage-7.10.7-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:0b944ee8459f515f28b851728ad224fa2d068f1513ef6b7ff1efafeb2185f999"}, - {file = "coverage-7.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4b583b97ab2e3efe1b3e75248a9b333bd3f8b0b1b8e5b45578e05e5850dfb2c2"}, - {file = "coverage-7.10.7-cp313-cp313t-win32.whl", hash = "sha256:2a78cd46550081a7909b3329e2266204d584866e8d97b898cd7fb5ac8d888b1a"}, - {file = "coverage-7.10.7-cp313-cp313t-win_amd64.whl", hash = "sha256:33a5e6396ab684cb43dc7befa386258acb2d7fae7f67330ebb85ba4ea27938eb"}, - {file = "coverage-7.10.7-cp313-cp313t-win_arm64.whl", hash = "sha256:86b0e7308289ddde73d863b7683f596d8d21c7d8664ce1dee061d0bcf3fbb4bb"}, - {file = "coverage-7.10.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b06f260b16ead11643a5a9f955bd4b5fd76c1a4c6796aeade8520095b75de520"}, - {file = "coverage-7.10.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:212f8f2e0612778f09c55dd4872cb1f64a1f2b074393d139278ce902064d5b32"}, - {file = "coverage-7.10.7-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3445258bcded7d4aa630ab8296dea4d3f15a255588dd535f980c193ab6b95f3f"}, - {file = "coverage-7.10.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb45474711ba385c46a0bfe696c695a929ae69ac636cda8f532be9e8c93d720a"}, - {file = "coverage-7.10.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:813922f35bd800dca9994c5971883cbc0d291128a5de6b167c7aa697fcf59360"}, - {file = "coverage-7.10.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:93c1b03552081b2a4423091d6fb3787265b8f86af404cff98d1b5342713bdd69"}, - {file = "coverage-7.10.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cc87dd1b6eaf0b848eebb1c86469b9f72a1891cb42ac7adcfbce75eadb13dd14"}, - {file = "coverage-7.10.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:39508ffda4f343c35f3236fe8d1a6634a51f4581226a1262769d7f970e73bffe"}, - {file = "coverage-7.10.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:925a1edf3d810537c5a3abe78ec5530160c5f9a26b1f4270b40e62cc79304a1e"}, - {file = "coverage-7.10.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2c8b9a0636f94c43cd3576811e05b89aa9bc2d0a85137affc544ae5cb0e4bfbd"}, - {file = "coverage-7.10.7-cp314-cp314-win32.whl", hash = "sha256:b7b8288eb7cdd268b0304632da8cb0bb93fadcfec2fe5712f7b9cc8f4d487be2"}, - {file = "coverage-7.10.7-cp314-cp314-win_amd64.whl", hash = "sha256:1ca6db7c8807fb9e755d0379ccc39017ce0a84dcd26d14b5a03b78563776f681"}, - {file = "coverage-7.10.7-cp314-cp314-win_arm64.whl", hash = "sha256:097c1591f5af4496226d5783d036bf6fd6cd0cbc132e071b33861de756efb880"}, - {file = "coverage-7.10.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a62c6ef0d50e6de320c270ff91d9dd0a05e7250cac2a800b7784bae474506e63"}, - {file = "coverage-7.10.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9fa6e4dd51fe15d8738708a973470f67a855ca50002294852e9571cdbd9433f2"}, - {file = "coverage-7.10.7-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8fb190658865565c549b6b4706856d6a7b09302c797eb2cf8e7fe9dabb043f0d"}, - {file = "coverage-7.10.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:affef7c76a9ef259187ef31599a9260330e0335a3011732c4b9effa01e1cd6e0"}, - {file = "coverage-7.10.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e16e07d85ca0cf8bafe5f5d23a0b850064e8e945d5677492b06bbe6f09cc699"}, - {file = "coverage-7.10.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03ffc58aacdf65d2a82bbeb1ffe4d01ead4017a21bfd0454983b88ca73af94b9"}, - {file = "coverage-7.10.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1b4fd784344d4e52647fd7857b2af5b3fbe6c239b0b5fa63e94eb67320770e0f"}, - {file = "coverage-7.10.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0ebbaddb2c19b71912c6f2518e791aa8b9f054985a0769bdb3a53ebbc765c6a1"}, - {file = "coverage-7.10.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a2d9a3b260cc1d1dbdb1c582e63ddcf5363426a1a68faa0f5da28d8ee3c722a0"}, - {file = "coverage-7.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a3cc8638b2480865eaa3926d192e64ce6c51e3d29c849e09d5b4ad95efae5399"}, - {file = "coverage-7.10.7-cp314-cp314t-win32.whl", hash = "sha256:67f8c5cbcd3deb7a60b3345dffc89a961a484ed0af1f6f73de91705cc6e31235"}, - {file = "coverage-7.10.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e1ed71194ef6dea7ed2d5cb5f7243d4bcd334bfb63e59878519be558078f848d"}, - {file = "coverage-7.10.7-cp314-cp314t-win_arm64.whl", hash = "sha256:7fe650342addd8524ca63d77b2362b02345e5f1a093266787d210c70a50b471a"}, - {file = "coverage-7.10.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fff7b9c3f19957020cac546c70025331113d2e61537f6e2441bc7657913de7d3"}, - {file = "coverage-7.10.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bc91b314cef27742da486d6839b677b3f2793dfe52b51bbbb7cf736d5c29281c"}, - {file = "coverage-7.10.7-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:567f5c155eda8df1d3d439d40a45a6a5f029b429b06648235f1e7e51b522b396"}, - {file = "coverage-7.10.7-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2af88deffcc8a4d5974cf2d502251bc3b2db8461f0b66d80a449c33757aa9f40"}, - {file = "coverage-7.10.7-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7315339eae3b24c2d2fa1ed7d7a38654cba34a13ef19fbcb9425da46d3dc594"}, - {file = "coverage-7.10.7-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:912e6ebc7a6e4adfdbb1aec371ad04c68854cd3bf3608b3514e7ff9062931d8a"}, - {file = "coverage-7.10.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f49a05acd3dfe1ce9715b657e28d138578bc40126760efb962322c56e9ca344b"}, - {file = "coverage-7.10.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:cce2109b6219f22ece99db7644b9622f54a4e915dad65660ec435e89a3ea7cc3"}, - {file = "coverage-7.10.7-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:f3c887f96407cea3916294046fc7dab611c2552beadbed4ea901cbc6a40cc7a0"}, - {file = "coverage-7.10.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:635adb9a4507c9fd2ed65f39693fa31c9a3ee3a8e6dc64df033e8fdf52a7003f"}, - {file = "coverage-7.10.7-cp39-cp39-win32.whl", hash = "sha256:5a02d5a850e2979b0a014c412573953995174743a3f7fa4ea5a6e9a3c5617431"}, - {file = "coverage-7.10.7-cp39-cp39-win_amd64.whl", hash = "sha256:c134869d5ffe34547d14e174c866fd8fe2254918cc0a95e99052903bc1543e07"}, - {file = "coverage-7.10.7-py3-none-any.whl", hash = "sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260"}, - {file = "coverage-7.10.7.tar.gz", hash = "sha256:f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239"}, + {file = "coverage-7.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:02d9fb9eccd48f6843c98a37bd6817462f130b86da8660461e8f5e54d4c06070"}, + {file = "coverage-7.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:367449cf07d33dc216c083f2036bb7d976c6e4903ab31be400ad74ad9f85ce98"}, + {file = "coverage-7.13.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cdb3c9f8fef0a954c632f64328a3935988d33a6604ce4bf67ec3e39670f12ae5"}, + {file = "coverage-7.13.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d10fd186aac2316f9bbb46ef91977f9d394ded67050ad6d84d94ed6ea2e8e54e"}, + {file = "coverage-7.13.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f88ae3e69df2ab62fb0bc5219a597cb890ba5c438190ffa87490b315190bb33"}, + {file = "coverage-7.13.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c4be718e51e86f553bcf515305a158a1cd180d23b72f07ae76d6017c3cc5d791"}, + {file = "coverage-7.13.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a00d3a393207ae12f7c49bb1c113190883b500f48979abb118d8b72b8c95c032"}, + {file = "coverage-7.13.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a7b1cd820e1b6116f92c6128f1188e7afe421c7e1b35fa9836b11444e53ebd9"}, + {file = "coverage-7.13.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:37eee4e552a65866f15dedd917d5e5f3d59805994260720821e2c1b51ac3248f"}, + {file = "coverage-7.13.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:62d7c4f13102148c78d7353c6052af6d899a7f6df66a32bddcc0c0eb7c5326f8"}, + {file = "coverage-7.13.0-cp310-cp310-win32.whl", hash = "sha256:24e4e56304fdb56f96f80eabf840eab043b3afea9348b88be680ec5986780a0f"}, + {file = "coverage-7.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:74c136e4093627cf04b26a35dab8cbfc9b37c647f0502fc313376e11726ba303"}, + {file = "coverage-7.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0dfa3855031070058add1a59fdfda0192fd3e8f97e7c81de0596c145dea51820"}, + {file = "coverage-7.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4fdb6f54f38e334db97f72fa0c701e66d8479af0bc3f9bfb5b90f1c30f54500f"}, + {file = "coverage-7.13.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7e442c013447d1d8d195be62852270b78b6e255b79b8675bad8479641e21fd96"}, + {file = "coverage-7.13.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ed5630d946859de835a85e9a43b721123a8a44ec26e2830b296d478c7fd4259"}, + {file = "coverage-7.13.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f15a931a668e58087bc39d05d2b4bf4b14ff2875b49c994bbdb1c2217a8daeb"}, + {file = "coverage-7.13.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:30a3a201a127ea57f7e14ba43c93c9c4be8b7d17a26e03bb49e6966d019eede9"}, + {file = "coverage-7.13.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7a485ff48fbd231efa32d58f479befce52dcb6bfb2a88bb7bf9a0b89b1bc8030"}, + {file = "coverage-7.13.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:22486cdafba4f9e471c816a2a5745337742a617fef68e890d8baf9f3036d7833"}, + {file = "coverage-7.13.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:263c3dbccc78e2e331e59e90115941b5f53e85cfcc6b3b2fbff1fd4e3d2c6ea8"}, + {file = "coverage-7.13.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e5330fa0cc1f5c3c4c3bb8e101b742025933e7848989370a1d4c8c5e401ea753"}, + {file = "coverage-7.13.0-cp311-cp311-win32.whl", hash = "sha256:0f4872f5d6c54419c94c25dd6ae1d015deeb337d06e448cd890a1e89a8ee7f3b"}, + {file = "coverage-7.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51a202e0f80f241ccb68e3e26e19ab5b3bf0f813314f2c967642f13ebcf1ddfe"}, + {file = "coverage-7.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:d2a9d7f1c11487b1c69367ab3ac2d81b9b3721f097aa409a3191c3e90f8f3dd7"}, + {file = "coverage-7.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0b3d67d31383c4c68e19a88e28fc4c2e29517580f1b0ebec4a069d502ce1e0bf"}, + {file = "coverage-7.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:581f086833d24a22c89ae0fe2142cfaa1c92c930adf637ddf122d55083fb5a0f"}, + {file = "coverage-7.13.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0a3a30f0e257df382f5f9534d4ce3d4cf06eafaf5192beb1a7bd066cb10e78fb"}, + {file = "coverage-7.13.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:583221913fbc8f53b88c42e8dbb8fca1d0f2e597cb190ce45916662b8b9d9621"}, + {file = "coverage-7.13.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f5d9bd30756fff3e7216491a0d6d520c448d5124d3d8e8f56446d6412499e74"}, + {file = "coverage-7.13.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a23e5a1f8b982d56fa64f8e442e037f6ce29322f1f9e6c2344cd9e9f4407ee57"}, + {file = "coverage-7.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9b01c22bc74a7fb44066aaf765224c0d933ddf1f5047d6cdfe4795504a4493f8"}, + {file = "coverage-7.13.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:898cce66d0836973f48dda4e3514d863d70142bdf6dfab932b9b6a90ea5b222d"}, + {file = "coverage-7.13.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:3ab483ea0e251b5790c2aac03acde31bff0c736bf8a86829b89382b407cd1c3b"}, + {file = "coverage-7.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1d84e91521c5e4cb6602fe11ece3e1de03b2760e14ae4fcf1a4b56fa3c801fcd"}, + {file = "coverage-7.13.0-cp312-cp312-win32.whl", hash = "sha256:193c3887285eec1dbdb3f2bd7fbc351d570ca9c02ca756c3afbc71b3c98af6ef"}, + {file = "coverage-7.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:4f3e223b2b2db5e0db0c2b97286aba0036ca000f06aca9b12112eaa9af3d92ae"}, + {file = "coverage-7.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:086cede306d96202e15a4b77ace8472e39d9f4e5f9fd92dd4fecdfb2313b2080"}, + {file = "coverage-7.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:28ee1c96109974af104028a8ef57cec21447d42d0e937c0275329272e370ebcf"}, + {file = "coverage-7.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d1e97353dcc5587b85986cda4ff3ec98081d7e84dd95e8b2a6d59820f0545f8a"}, + {file = "coverage-7.13.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:99acd4dfdfeb58e1937629eb1ab6ab0899b131f183ee5f23e0b5da5cba2fec74"}, + {file = "coverage-7.13.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ff45e0cd8451e293b63ced93161e189780baf444119391b3e7d25315060368a6"}, + {file = "coverage-7.13.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f4f72a85316d8e13234cafe0a9f81b40418ad7a082792fa4165bd7d45d96066b"}, + {file = "coverage-7.13.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:11c21557d0e0a5a38632cbbaca5f008723b26a89d70db6315523df6df77d6232"}, + {file = "coverage-7.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76541dc8d53715fb4f7a3a06b34b0dc6846e3c69bc6204c55653a85dd6220971"}, + {file = "coverage-7.13.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6e9e451dee940a86789134b6b0ffbe31c454ade3b849bb8a9d2cca2541a8e91d"}, + {file = "coverage-7.13.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:5c67dace46f361125e6b9cace8fe0b729ed8479f47e70c89b838d319375c8137"}, + {file = "coverage-7.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f59883c643cb19630500f57016f76cfdcd6845ca8c5b5ea1f6e17f74c8e5f511"}, + {file = "coverage-7.13.0-cp313-cp313-win32.whl", hash = "sha256:58632b187be6f0be500f553be41e277712baa278147ecb7559983c6d9faf7ae1"}, + {file = "coverage-7.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:73419b89f812f498aca53f757dd834919b48ce4799f9d5cad33ca0ae442bdb1a"}, + {file = "coverage-7.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:eb76670874fdd6091eedcc856128ee48c41a9bbbb9c3f1c7c3cf169290e3ffd6"}, + {file = "coverage-7.13.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6e63ccc6e0ad8986386461c3c4b737540f20426e7ec932f42e030320896c311a"}, + {file = "coverage-7.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:494f5459ffa1bd45e18558cd98710c36c0b8fbfa82a5eabcbe671d80ecffbfe8"}, + {file = "coverage-7.13.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:06cac81bf10f74034e055e903f5f946e3e26fc51c09fc9f584e4a1605d977053"}, + {file = "coverage-7.13.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f2ffc92b46ed6e6760f1d47a71e56b5664781bc68986dbd1836b2b70c0ce2071"}, + {file = "coverage-7.13.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0602f701057c6823e5db1b74530ce85f17c3c5be5c85fc042ac939cbd909426e"}, + {file = "coverage-7.13.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:25dc33618d45456ccb1d37bce44bc78cf269909aa14c4db2e03d63146a8a1493"}, + {file = "coverage-7.13.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:71936a8b3b977ddd0b694c28c6a34f4fff2e9dd201969a4ff5d5fc7742d614b0"}, + {file = "coverage-7.13.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:936bc20503ce24770c71938d1369461f0c5320830800933bc3956e2a4ded930e"}, + {file = "coverage-7.13.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:af0a583efaacc52ae2521f8d7910aff65cdb093091d76291ac5820d5e947fc1c"}, + {file = "coverage-7.13.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f1c23e24a7000da892a312fb17e33c5f94f8b001de44b7cf8ba2e36fbd15859e"}, + {file = "coverage-7.13.0-cp313-cp313t-win32.whl", hash = "sha256:5f8a0297355e652001015e93be345ee54393e45dc3050af4a0475c5a2b767d46"}, + {file = "coverage-7.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6abb3a4c52f05e08460bd9acf04fec027f8718ecaa0d09c40ffbc3fbd70ecc39"}, + {file = "coverage-7.13.0-cp313-cp313t-win_arm64.whl", hash = "sha256:3ad968d1e3aa6ce5be295ab5fe3ae1bf5bb4769d0f98a80a0252d543a2ef2e9e"}, + {file = "coverage-7.13.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:453b7ec753cf5e4356e14fe858064e5520c460d3bbbcb9c35e55c0d21155c256"}, + {file = "coverage-7.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:af827b7cbb303e1befa6c4f94fd2bf72f108089cfa0f8abab8f4ca553cf5ca5a"}, + {file = "coverage-7.13.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9987a9e4f8197a1000280f7cc089e3ea2c8b3c0a64d750537809879a7b4ceaf9"}, + {file = "coverage-7.13.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3188936845cd0cb114fa6a51842a304cdbac2958145d03be2377ec41eb285d19"}, + {file = "coverage-7.13.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2bdb3babb74079f021696cb46b8bb5f5661165c385d3a238712b031a12355be"}, + {file = "coverage-7.13.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7464663eaca6adba4175f6c19354feea61ebbdd735563a03d1e472c7072d27bb"}, + {file = "coverage-7.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8069e831f205d2ff1f3d355e82f511eb7c5522d7d413f5db5756b772ec8697f8"}, + {file = "coverage-7.13.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:6fb2d5d272341565f08e962cce14cdf843a08ac43bd621783527adb06b089c4b"}, + {file = "coverage-7.13.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5e70f92ef89bac1ac8a99b3324923b4749f008fdbd7aa9cb35e01d7a284a04f9"}, + {file = "coverage-7.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4b5de7d4583e60d5fd246dd57fcd3a8aa23c6e118a8c72b38adf666ba8e7e927"}, + {file = "coverage-7.13.0-cp314-cp314-win32.whl", hash = "sha256:a6c6e16b663be828a8f0b6c5027d36471d4a9f90d28444aa4ced4d48d7d6ae8f"}, + {file = "coverage-7.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:0900872f2fdb3ee5646b557918d02279dc3af3dfb39029ac4e945458b13f73bc"}, + {file = "coverage-7.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:3a10260e6a152e5f03f26db4a407c4c62d3830b9af9b7c0450b183615f05d43b"}, + {file = "coverage-7.13.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9097818b6cc1cfb5f174e3263eba4a62a17683bcfe5c4b5d07f4c97fa51fbf28"}, + {file = "coverage-7.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0018f73dfb4301a89292c73be6ba5f58722ff79f51593352759c1790ded1cabe"}, + {file = "coverage-7.13.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:166ad2a22ee770f5656e1257703139d3533b4a0b6909af67c6b4a3adc1c98657"}, + {file = "coverage-7.13.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f6aaef16d65d1787280943f1c8718dc32e9cf141014e4634d64446702d26e0ff"}, + {file = "coverage-7.13.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e999e2dcc094002d6e2c7bbc1fb85b58ba4f465a760a8014d97619330cdbbbf3"}, + {file = "coverage-7.13.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:00c3d22cf6fb1cf3bf662aaaa4e563be8243a5ed2630339069799835a9cc7f9b"}, + {file = "coverage-7.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22ccfe8d9bb0d6134892cbe1262493a8c70d736b9df930f3f3afae0fe3ac924d"}, + {file = "coverage-7.13.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:9372dff5ea15930fea0445eaf37bbbafbc771a49e70c0aeed8b4e2c2614cc00e"}, + {file = "coverage-7.13.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:69ac2c492918c2461bc6ace42d0479638e60719f2a4ef3f0815fa2df88e9f940"}, + {file = "coverage-7.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:739c6c051a7540608d097b8e13c76cfa85263ced467168dc6b477bae3df7d0e2"}, + {file = "coverage-7.13.0-cp314-cp314t-win32.whl", hash = "sha256:fe81055d8c6c9de76d60c94ddea73c290b416e061d40d542b24a5871bad498b7"}, + {file = "coverage-7.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:445badb539005283825959ac9fa4a28f712c214b65af3a2c464f1adc90f5fcbc"}, + {file = "coverage-7.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:de7f6748b890708578fc4b7bb967d810aeb6fcc9bff4bb77dbca77dab2f9df6a"}, + {file = "coverage-7.13.0-py3-none-any.whl", hash = "sha256:850d2998f380b1e266459ca5b47bc9e7daf9af1d070f66317972f382d46f1904"}, + {file = "coverage-7.13.0.tar.gz", hash = "sha256:a394aa27f2d7ff9bc04cf703817773a59ad6dfbd577032e690f961d2460ee936"}, ] [package.dependencies] @@ -438,33 +357,32 @@ toml = ["tomli ; python_full_version <= \"3.11.0a6\""] [[package]] name = "docstrfmt" -version = "1.11.1" +version = "2.0.1" description = "docstrfmt: A formatter for Sphinx flavored reStructuredText." optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["lint"] files = [ - {file = "docstrfmt-1.11.1-py3-none-any.whl", hash = "sha256:6782d8663321c3a7c40be08a36fbcb1ea9e46d1efba85411ba807d97f384871a"}, - {file = "docstrfmt-1.11.1.tar.gz", hash = "sha256:d41e19d6c5d524cc7f8ff6cbfecb8762d77e696b9fe4f5057269051fb966fc80"}, + {file = "docstrfmt-2.0.1-py3-none-any.whl", hash = "sha256:88f7c4bda3683b2464ef38f339a6f30c5fb36b94cfe1c2853a225037d198447a"}, + {file = "docstrfmt-2.0.1.tar.gz", hash = "sha256:98895f70168aaf9ca92e6dc946e1f965e79d83d5966144f6f1a78c46184967de"}, ] [package.dependencies] black = ">=24" click = ">=8" -docutils = ">=0.20" +coverage = ">=7.11.0" +docutils = ">=0.21" +docutils-stubs = "0.0.22" libcst = ">=1" platformdirs = ">=4" roman = "*" sphinx = ">=7" tabulate = ">=0.9" -toml = {version = ">=0.10", markers = "python_version < \"3.11\""} +tomli = {version = ">=0.10", markers = "python_version < \"3.11\""} +types-docutils = "0.22.3.20251115" [package.extras] -ci = ["coveralls"] -d = ["aiohttp (>=3)"] -dev = ["docstrfmt[lint]", "docstrfmt[test]", "packaging"] -lint = ["pre-commit", "ruff (>=0.0.292)"] -test = ["pytest", "pytest-aiohttp"] +d = ["aiohttp"] [[package]] name = "docutils" @@ -479,6 +397,21 @@ files = [ ] markers = {main = "extra == \"docs\""} +[[package]] +name = "docutils-stubs" +version = "0.0.22" +description = "PEP 561 type stubs for docutils" +optional = false +python-versions = ">=3.5" +groups = ["lint"] +files = [ + {file = "docutils-stubs-0.0.22.tar.gz", hash = "sha256:1736d9650cfc20cff8c72582806c33a5c642694e2df9e430717e7da7e73efbdf"}, + {file = "docutils_stubs-0.0.22-py3-none-any.whl", hash = "sha256:157807309de24e8c96af9a13afe207410f1fc6e5aab5d974fd6b9191f04de327"}, +] + +[package.dependencies] +docutils = ">=0.14" + [[package]] name = "exceptiongroup" version = "1.3.0" @@ -486,7 +419,7 @@ description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" groups = ["dev"] -markers = "python_version < \"3.11\"" +markers = "python_version == \"3.10\"" files = [ {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, @@ -527,31 +460,6 @@ files = [ ] markers = {main = "extra == \"docs\""} -[[package]] -name = "importlib-metadata" -version = "8.7.0" -description = "Read metadata from Python packages" -optional = false -python-versions = ">=3.9" -groups = ["main", "lint"] -files = [ - {file = "importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd"}, - {file = "importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000"}, -] -markers = {main = "extra == \"docs\" and python_version == \"3.9\"", lint = "python_version == \"3.9\""} - -[package.dependencies] -zipp = ">=3.20" - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=2.2)"] -perf = ["ipython"] -test = ["flufl.flake8", "importlib_resources (>=1.3) ; python_version < \"3.9\"", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] -type = ["pytest-mypy"] - [[package]] name = "iniconfig" version = "2.1.0" @@ -664,7 +572,6 @@ pyyaml = [ {version = ">=6.0.3", markers = "python_version >= \"3.14\""}, ] pyyaml-ft = {version = ">=8.0.0", markers = "python_version == \"3.13\""} -typing-extensions = {version = "*", markers = "python_version < \"3.10\""} [[package]] name = "librt" @@ -952,24 +859,6 @@ files = [ {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, ] -[[package]] -name = "platformdirs" -version = "4.4.0" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." -optional = false -python-versions = ">=3.9" -groups = ["lint"] -markers = "python_version < \"3.13\"" -files = [ - {file = "platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85"}, - {file = "platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf"}, -] - -[package.extras] -docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"] -type = ["mypy (>=1.14.1)"] - [[package]] name = "platformdirs" version = "4.5.1" @@ -977,7 +866,6 @@ description = "A small Python package for determining appropriate platform-speci optional = false python-versions = ">=3.10" groups = ["lint"] -markers = "python_version >= \"3.13\"" files = [ {file = "platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31"}, {file = "platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda"}, @@ -1351,19 +1239,6 @@ urllib3 = ">=1.21.1,<3" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] -[[package]] -name = "roman" -version = "5.1" -description = "Integer to Roman numerals converter" -optional = false -python-versions = ">=3.9" -groups = ["lint"] -markers = "python_version < \"3.13\"" -files = [ - {file = "roman-5.1-py3-none-any.whl", hash = "sha256:bf595d8a9bc4a8e8b1dfa23e1d4def0251b03b494786df6b8c3d3f1635ce285a"}, - {file = "roman-5.1.tar.gz", hash = "sha256:3a86572e9bc9183e771769601189e5fa32f1620ffeceebb9eca836affb409986"}, -] - [[package]] name = "roman" version = "5.2" @@ -1371,7 +1246,6 @@ description = "Integer to Roman numerals converter" optional = false python-versions = ">=3.10" groups = ["lint"] -markers = "python_version >= \"3.13\"" files = [ {file = "roman-5.2-py3-none-any.whl", hash = "sha256:89d3b47400388806d06ff77ea77c79ab080bc127820dea6bf34e1f1c1b8e676e"}, {file = "roman-5.2.tar.gz", hash = "sha256:275fe9f46290f7d0ffaea1c33251b92b8e463ace23660508ceef522e7587cb6f"}, @@ -1438,7 +1312,6 @@ babel = ">=2.13" colorama = {version = ">=0.4.6", markers = "sys_platform == \"win32\""} docutils = ">=0.20,<0.22" imagesize = ">=1.3" -importlib-metadata = {version = ">=6.0", markers = "python_version < \"3.10\""} Jinja2 = ">=3.1" packaging = ">=23.0" Pygments = ">=2.17" @@ -1457,26 +1330,6 @@ docs = ["sphinxcontrib-websupport"] lint = ["flake8 (>=6.0)", "importlib-metadata (>=6.0)", "mypy (==1.10.1)", "pytest (>=6.0)", "ruff (==0.5.2)", "sphinx-lint (>=0.9)", "tomli (>=2)", "types-docutils (==0.21.0.20240711)", "types-requests (>=2.30.0)"] test = ["cython (>=3.0)", "defusedxml (>=0.7.1)", "pytest (>=8.0)", "setuptools (>=70.0)", "typing_extensions (>=4.9)"] -[[package]] -name = "sphinx-lint" -version = "1.0.0" -description = "Check for stylistic and formal issues in .rst and .py files included in the documentation." -optional = false -python-versions = ">=3.8" -groups = ["lint"] -markers = "python_version < \"3.13\"" -files = [ - {file = "sphinx_lint-1.0.0-py3-none-any.whl", hash = "sha256:6117a0f340b2dc73eadfc57db7531d4477e0929f92a0c1a2f61e6edbc272f0bc"}, - {file = "sphinx_lint-1.0.0.tar.gz", hash = "sha256:6eafdb44172ce526f405bf36c713eb246f1340ec2d667e7298e2487ed76decd2"}, -] - -[package.dependencies] -polib = "*" -regex = "*" - -[package.extras] -tests = ["pytest", "pytest-cov"] - [[package]] name = "sphinx-lint" version = "1.0.2" @@ -1484,7 +1337,6 @@ description = "Check for stylistic and formal issues in .rst and .py files inclu optional = false python-versions = ">=3.10" groups = ["lint"] -markers = "python_version >= \"3.13\"" files = [ {file = "sphinx_lint-1.0.2-py3-none-any.whl", hash = "sha256:edcd0fa4d916386c5a3ef7ef0f5136f0bb4a15feefc83c1068ba15bc16eec652"}, {file = "sphinx_lint-1.0.2.tar.gz", hash = "sha256:4e7fc12f44f750b0006eaad237d7db9b1d8aba92adda9c838af891654b371d35"}, @@ -1655,19 +1507,6 @@ files = [ [package.extras] widechars = ["wcwidth"] -[[package]] -name = "toml" -version = "0.10.2" -description = "Python Library for Tom's Obvious, Minimal Language" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -groups = ["lint"] -markers = "python_version < \"3.11\"" -files = [ - {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, - {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, -] - [[package]] name = "tomli" version = "2.3.0" @@ -1719,7 +1558,19 @@ files = [ {file = "tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b"}, {file = "tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549"}, ] -markers = {main = "extra == \"docs\" and python_version < \"3.11\"", dev = "python_full_version <= \"3.11.0a6\"", lint = "python_version < \"3.11\""} +markers = {main = "extra == \"docs\" and python_version == \"3.10\"", dev = "python_full_version <= \"3.11.0a6\"", lint = "python_version == \"3.10\""} + +[[package]] +name = "types-docutils" +version = "0.22.3.20251115" +description = "Typing stubs for docutils" +optional = false +python-versions = ">=3.9" +groups = ["lint"] +files = [ + {file = "types_docutils-0.22.3.20251115-py3-none-any.whl", hash = "sha256:c6e53715b65395d00a75a3a8a74e352c669bc63959e65a207dffaa22f4a2ad6e"}, + {file = "types_docutils-0.22.3.20251115.tar.gz", hash = "sha256:0f79ea6a7bd4d12d56c9f824a0090ffae0ea4204203eb0006392906850913e16"}, +] [[package]] name = "types-pyyaml" @@ -1744,7 +1595,7 @@ files = [ {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, ] -markers = {dev = "python_version < \"3.11\""} +markers = {dev = "python_version == \"3.10\""} [[package]] name = "urllib3" @@ -1765,31 +1616,10 @@ h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] -[[package]] -name = "zipp" -version = "3.23.0" -description = "Backport of pathlib-compatible object wrapper for zip files" -optional = false -python-versions = ">=3.9" -groups = ["main", "lint"] -files = [ - {file = "zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e"}, - {file = "zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166"}, -] -markers = {main = "extra == \"docs\" and python_version == \"3.9\"", lint = "python_version == \"3.9\""} - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=2.2)"] -test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more_itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] -type = ["pytest-mypy"] - [extras] docs = ["sphinx", "sphinx-rtd-theme"] [metadata] lock-version = "2.1" -python-versions = ">=3.9" -content-hash = "830a887c8b325f6d0043ca3e4c982526f8a2fefc22c16de302164f2f5a6a238b" +python-versions = ">=3.10" +content-hash = "a66b26695a7b01066dcc6a13ae4fe8a2b09cf25853594d2b6adc3a63dcbdf4ac" diff --git a/pyproject.toml b/pyproject.toml index 2a333fe..ba12bae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,12 +5,11 @@ description = "Painless YAML config files" authors = [{ name = "Adrian Sampson", email = "adrian@radbox.org" }] readme = "README.rst" license = "MIT" -requires-python = ">=3.9" +requires-python = ">=3.10" classifiers = [ "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", @@ -27,7 +26,7 @@ dev = [ "pytest-cov >= 7.0.0", ] lint = [ - "docstrfmt >= 1.11.1", + "docstrfmt >= 2.0.1", "mypy >= 1.18.2", "ruff >= 0.6.4", "sphinx-lint >= 1.0.0", @@ -79,7 +78,8 @@ cmd = "ruff format" [tool.poe.tasks.format-docs] help = "Format the documentation" -cmd = "docstrfmt docs *.rst" +# TODO: Remove --no-format-python-code-blocks once https://github.com/LilSpazJoekp/docstrfmt/issues/171 is resolved +cmd = "docstrfmt --no-format-python-code-blocks --preserve-adornments docs *.rst" [tool.poe.tasks.lint] help = "Check the code for linting issues. Accepts ruff options." @@ -114,7 +114,7 @@ env.OPTS = """ line-length = 80 [tool.ruff] -target-version = "py39" +target-version = "py310" [tool.ruff.lint] future-annotations = true From 89691e0917a21b37d3550cb73ebd8489ac05ccd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= Date: Mon, 29 Dec 2025 17:01:32 +0000 Subject: [PATCH 10/15] Try removing --only=lint --- .github/workflows/lint.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index a4169b4..1209a9f 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -65,7 +65,7 @@ jobs: cache: poetry - name: Install dependencies - run: poetry install --only=lint + run: poetry install - name: Check code formatting # the job output will contain colored diffs with what needs adjusting @@ -86,7 +86,7 @@ jobs: cache: poetry - name: Install dependencies - run: poetry install --only=lint + run: poetry install - name: Lint code run: poe lint --output-format=github ${{ needs.changed-files.outputs.changed_python_files }} @@ -106,7 +106,7 @@ jobs: cache: poetry - name: Install dependencies - run: poetry install --only=lint + run: poetry install - name: Type check code uses: liskin/gh-problem-matcher-wrap@v3 From b86ec28c607941e32fc4fe68bbcb9bd7051107d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= Date: Mon, 5 Jan 2026 03:19:26 +0000 Subject: [PATCH 11/15] Enable coverage --- .github/workflows/main.yml | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2ac9395..f4261e5 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,11 +1,15 @@ -name: Python Tests - -on: [push] - +name: Test +on: + pull_request: + push: + branches: + - main jobs: test: name: Run tests + permissions: + id-token: write strategy: fail-fast: false matrix: @@ -13,12 +17,12 @@ jobs: python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] runs-on: ${{ matrix.platform }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Install Python tools uses: BrandonLWhite/pipx-install-action@v1.0.3 - name: Setup Python with poetry caching # poetry cache requires poetry to already be installed, weirdly - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} cache: poetry @@ -26,4 +30,16 @@ jobs: - name: Test run: |- poetry install - poe test + poe test-with-coverage + + - name: Upload test results to Codecov + uses: codecov/test-results-action@v1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + + - name: Upload code coverage + uses: codecov/codecov-action@v5 + with: + files: ./coverage.xml + flags: ${{ matrix.platform}}_python${{ matrix.python-version }} + use_oidc: ${{ !(github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork) }} From f8d8b3d05d1952f024103020b287454c0c35204a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= Date: Mon, 5 Jan 2026 03:26:12 +0000 Subject: [PATCH 12/15] Configure pytest --- setup.cfg | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/setup.cfg b/setup.cfg index d83a97c..270324a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,3 +1,14 @@ +[tool:pytest] +# slightly more verbose output +console_output_style = count +# pretty-print test names in the Codecov U +junit_family = legacy +addopts = + # show all skipped/failed/xfailed tests in the summary except passed + -ra + --strict-config + --junitxml=.reports/pytest.xml + [coverage:run] data_file = .reports/coverage/data branch = true From c19683439dbb1d6a32676525df2b7f0b2fd1154f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= Date: Mon, 29 Dec 2025 11:38:53 +0000 Subject: [PATCH 13/15] Add git blame ignore revs --- .git-blame-ignore-revs | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .git-blame-ignore-revs diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000..758eb34 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,9 @@ +# 2025 +# Reformat the codebase +2bb508ef08a0acfd0c8f8b6f8b48a240d2309740 +# Fix linting issues +23bcec4bcabe3f1718b90e89d85cfa53d36a4445 +# Format docs +424a17ceeccf93d92cb2e6b4062907af858854b1 +# Update URLs in the docs +8aa6d237ac6479971ffe38608b2526ef0abe55f4 From e77eb97185bcda273e77cf894040832257f1ef38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= Date: Mon, 5 Jan 2026 03:55:03 +0000 Subject: [PATCH 14/15] Add some earlier clean-up commits to .git-blame-ignore-revs --- .git-blame-ignore-revs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index 758eb34..e716518 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -1,4 +1,13 @@ # 2025 +# adapt super() calls to Python 3 +97c43eb36fa04c8dde2bb5acacdb00782697ad86 +# clean up from Python 3 transition: class XYZ(object) -> class XYZ +142d3d9226228e8fc9cac73013990b1ff41d9782 +# remove u'' string prefixes +d41faf0bdf0ba5e899625c0f2fd2421aaad8ff1c +# style +94309b91a070d0f64755fe0a7c83e9908b5de42e + # Reformat the codebase 2bb508ef08a0acfd0c8f8b6f8b48a240d2309740 # Fix linting issues From 6cfcba971b168d6375b671cc6d61f6ed70051e23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= Date: Mon, 5 Jan 2026 13:16:06 +0000 Subject: [PATCH 15/15] Upgrade GitHub actions --- .github/workflows/changelog_reminder.yaml | 8 ++++---- .github/workflows/lint.yaml | 14 +++++++------- .github/workflows/main.yml | 2 +- .github/workflows/make_release.yaml | 12 ++++++------ 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/.github/workflows/changelog_reminder.yaml b/.github/workflows/changelog_reminder.yaml index 4d95944..9ecbcd3 100644 --- a/.github/workflows/changelog_reminder.yaml +++ b/.github/workflows/changelog_reminder.yaml @@ -13,24 +13,24 @@ jobs: contents: read runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Get all updated Python files id: changed-python-files - uses: tj-actions/changed-files@v46 + uses: tj-actions/changed-files@v47 with: files: | **.py - name: Check for the changelog update id: changelog-update - uses: tj-actions/changed-files@v46 + uses: tj-actions/changed-files@v47 with: files: docs/changelog.rst - name: Comment under the PR with a reminder if: steps.changed-python-files.outputs.any_changed == 'true' && steps.changelog-update.outputs.any_changed == 'false' - uses: thollander/actions-comment-pull-request@v2 + uses: thollander/actions-comment-pull-request@v3 with: message: 'Thank you for the PR! The changelog has not been updated, so here is a friendly reminder to check if you need to add an entry.' GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' \ No newline at end of file diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 1209a9f..57381ed 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -24,16 +24,16 @@ jobs: changed_doc_files: ${{ steps.changed-doc-files.outputs.all_changed_files }} changed_python_files: ${{ steps.changed-python-files.outputs.all_changed_files }} steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Get changed docs files id: changed-doc-files - uses: tj-actions/changed-files@v46 + uses: tj-actions/changed-files@v47 with: files: | docs/** - name: Get changed python files id: raw-changed-python-files - uses: tj-actions/changed-files@v46 + uses: tj-actions/changed-files@v47 with: files: | **.py @@ -56,7 +56,7 @@ jobs: name: Check formatting needs: changed-files steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Install Python tools uses: BrandonLWhite/pipx-install-action@v1.0.3 - uses: actions/setup-python@v6 @@ -77,7 +77,7 @@ jobs: name: Check linting needs: changed-files steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Install Python tools uses: BrandonLWhite/pipx-install-action@v1.0.3 - uses: actions/setup-python@v6 @@ -97,7 +97,7 @@ jobs: name: Check types with mypy needs: changed-files steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Install Python tools uses: BrandonLWhite/pipx-install-action@v1.0.3 - uses: actions/setup-python@v6 @@ -120,7 +120,7 @@ jobs: name: Check docs needs: changed-files steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Install Python tools uses: BrandonLWhite/pipx-install-action@v1.0.3 - uses: actions/setup-python@v6 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f4261e5..282f91c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -17,7 +17,7 @@ jobs: python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] runs-on: ${{ matrix.platform }} steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Install Python tools uses: BrandonLWhite/pipx-install-action@v1.0.3 - name: Setup Python with poetry caching diff --git a/.github/workflows/make_release.yaml b/.github/workflows/make_release.yaml index b2e2b3c..922667c 100644 --- a/.github/workflows/make_release.yaml +++ b/.github/workflows/make_release.yaml @@ -17,10 +17,10 @@ jobs: name: Bump version, commit and create tag runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install Python tools uses: BrandonLWhite/pipx-install-action@v1.0.3 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: ${{ env.PYTHON_VERSION }} cache: poetry @@ -40,13 +40,13 @@ jobs: runs-on: ubuntu-latest needs: increment-version steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: ref: ${{ env.NEW_TAG }} - name: Install Python tools uses: BrandonLWhite/pipx-install-action@v1.0.3 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: ${{ env.PYTHON_VERSION }} cache: poetry @@ -55,7 +55,7 @@ jobs: run: poetry build - name: Store the package - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: python-package-distributions path: dist/ @@ -71,7 +71,7 @@ jobs: id-token: write steps: - name: Download all the dists - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v7 with: name: python-package-distributions path: dist/