diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000..e716518 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,18 @@ +# 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 +23bcec4bcabe3f1718b90e89d85cfa53d36a4445 +# Format docs +424a17ceeccf93d92cb2e6b4062907af858854b1 +# Update URLs in the docs +8aa6d237ac6479971ffe38608b2526ef0abe55f4 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/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 new file mode 100644 index 0000000..57381ed --- /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@v6 + - name: Get changed docs files + id: changed-doc-files + uses: tj-actions/changed-files@v47 + with: + files: | + docs/** + - name: Get changed python files + id: raw-changed-python-files + uses: tj-actions/changed-files@v47 + 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@v6 + - 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 + + - 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@v6 + - 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 + + - 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@v6 + - 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 + + - 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@v6 + - 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/.github/workflows/main.yml b/.github/workflows/main.yml index 7556a2f..282f91c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,22 +1,28 @@ -name: Python Tests - -on: [push] - +name: Test +on: + pull_request: + push: + branches: + - main jobs: test: name: Run tests - runs-on: ubuntu-latest + permissions: + id-token: write strategy: + fail-fast: false matrix: - python: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] + platform: [ubuntu-latest, windows-latest] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] + runs-on: ${{ matrix.platform }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - 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 @@ -24,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) }} diff --git a/.github/workflows/make_release.yaml b/.github/workflows/make_release.yaml index de02836..922667c 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 }} @@ -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/ diff --git a/README.rst b/README.rst index 7152a1a..04bb832 100644 --- a/README.rst +++ b/README.rst @@ -4,53 +4,46 @@ 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 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`_. + +.. _argparse: https://docs.python.org/dev/library/argparse.html -.. _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: https://docs.python.org/library/configparser.html + +.. _confuse's documentation: https://confuse.readthedocs.io/en/latest/usage.html + +.. _logging: https://docs.python.org/library/logging.html + +.. _mit license: https://opensource.org/license/mit + +.. _optparse: https://docs.python.org/dev/library/optparse.html + +.. _yaml: https://yaml.org/ diff --git a/confuse/__init__.py b/confuse/__init__.py index b061be4..6c41fda 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 .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 cde272a..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. # @@ -13,40 +12,49 @@ # 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", + "REDACTED_TOMBSTONE", + "ROOT_NAME", + "ConfigView", + "Configuration", + "LazyConfig", + "RootView", + "Subview", ] import errno import os -from pathlib import Path -from typing import Any, Iterable, Sequence, TypeVar -import yaml from collections import OrderedDict +from typing import TYPE_CHECKING, Any, TypeVar + +import yaml -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' +if TYPE_CHECKING: + from collections.abc import Iterable, Sequence + from pathlib import Path -REDACTED_TOMBSTONE = 'REDACTED' +CONFIG_FILENAME = "config.yaml" +DEFAULT_FILENAME = "config_default.yaml" +ROOT_NAME = "root" -R = TypeVar('R') +REDACTED_TOMBSTONE = "REDACTED" + +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 @@ -78,11 +86,10 @@ 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. - """ + """Determine whether the view has a setting in any source.""" try: self.first() except NotFoundError: @@ -104,12 +111,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 f"<{self.__class__.__name__}: {self.name}>" def __iter__(self): """Iterate over the keys of a dictionary view or the *subviews* @@ -117,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): @@ -163,7 +166,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 +174,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 +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. @@ -305,13 +299,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 +365,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 +373,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 +414,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 += f"#{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: @@ -457,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 @@ -473,18 +464,19 @@ 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 (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 +499,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 = f"{self.appname.upper()}DIR" if read: self.read() @@ -535,8 +527,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 +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 @@ -599,10 +592,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 +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): @@ -645,9 +639,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 +654,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 +679,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..1a4a87e 100644 --- a/confuse/exceptions.py +++ b/confuse/exceptions.py @@ -1,8 +1,13 @@ import yaml __all__ = [ - 'ConfigError', 'NotFoundError', 'ConfigValueError', 'ConfigTypeError', - 'ConfigTemplateError', 'ConfigReadError'] + "ConfigError", + "ConfigReadError", + "ConfigTemplateError", + "ConfigTypeError", + "ConfigValueError", + "NotFoundError", +] 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 = 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 ab297cf..a4961e5 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): +class ConfigSource(dict[str, object]): """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,17 +27,15 @@ 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( - 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 @@ -50,15 +49,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 +77,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 +117,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 +127,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 +140,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, f"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 +175,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..4c4c830 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,14 +51,14 @@ 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)) + raise exceptions.NotFoundError(f"{key_name} not found") # The value is not required. return self.default @@ -82,51 +81,46 @@ 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(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), + "" 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 - ) + self.fail(f"must be numeric, not {type(value).__name__}", view, True) 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 +141,14 @@ 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): """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 +156,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 f"Sequence({self.subtemplate!r})" class MappingValues(Template): @@ -180,6 +174,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 +191,12 @@ 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): - """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 +214,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 f"String({', '.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(f"must match the pattern {self.pattern}", view) if self.expand_vars: return os.path.expandvars(value) @@ -247,6 +238,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 +256,20 @@ 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( - [c.value for c in self.choices], value - ), - view + 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 - ), - view + f"must be one of {list(self.choices)!r}, not {value!r}", + view, ) if isinstance(self.choices, abc.Mapping): @@ -290,12 +278,12 @@ 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): - """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 +292,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 f"OneOf({', '.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: @@ -334,12 +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): @@ -348,6 +330,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 +345,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 +362,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 +391,21 @@ 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(f"must be a single string, mapping, or a list{x}", view, True) + return (super()._convert_value(k, view), super()._convert_value(v, view)) class Filename(Template): @@ -444,8 +422,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 +455,34 @@ 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 f"Filename({', '.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) - ) + 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), - view + (f'needs sibling value "{self.relative_to}" to expand relative path'), + view, ) old_template = {} @@ -520,14 +501,14 @@ 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( + 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)) + raise exceptions.ConfigTemplateError( + f"missing template for {self.relative_to}, needed to expand " + f"{view.name}'s relative path" + ) next_template.subtemplates[next_relative] = rel_to_template next_relative = rel_to_template.relative_to @@ -541,11 +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): @@ -559,8 +536,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 +555,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 +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 @@ -622,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})" ) @@ -633,6 +612,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,20 +623,18 @@ 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 + True, ) return value -class AttrDict(dict): +class AttrDict(dict[str, object]): """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 +646,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 +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/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..3796a5a 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, + f"expected a mapping node, but found {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, + f"found unacceptable key ({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,9 +82,9 @@ 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: + except (OSError, 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) @@ -205,21 +205,21 @@ 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) - key = line.split(':')[0].strip() + comment += f"{line}\n" + 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) + out_data += f"{line}\n" return out_data 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..d2266d7 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,98 +1,105 @@ Changelog ---------- +========= + +Unreleased +---------- + +- Drop support for Python 3.9. 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/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/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..e707d2d 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: https://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()`_. - -.. _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 + 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/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 ``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 +.. _argparse: https://docs.python.org/dev/library/argparse.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. +.. _optparse: https://docs.python.org/3/library/optparse.html +.. _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 +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: https://docs.python.org/3/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)) - -This example allows the constant to override the default verbosity -level, which would otherwise come from a configuration file. + my_logger.setLevel(config["verbosity"].get(int)) -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/ +.. _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 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: 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 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: https://docs.python.org/2/library/unittest.html#unittest.TestCase.setUp +.. _unittest: https://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: 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/poetry.lock b/poetry.lock index 7565b95..669e418 100644 --- a/poetry.lock +++ b/poetry.lock @@ -4,52 +4,104 @@ 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.12.0" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.10" +groups = ["lint"] +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" +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 = "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 +217,22 @@ 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.3.1" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.10" +groups = ["lint"] +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,125 +240,113 @@ 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" -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] @@ -299,18 +355,62 @@ tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.1 [package.extras] toml = ["tomli ; python_full_version <= \"3.11.0a6\""] +[[package]] +name = "docstrfmt" +version = "2.0.1" +description = "docstrfmt: A formatter for Sphinx flavored reStructuredText." +optional = false +python-versions = ">=3.10" +groups = ["lint"] +files = [ + {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" +coverage = ">=7.11.0" +docutils = ">=0.21" +docutils-stubs = "0.0.22" +libcst = ">=1" +platformdirs = ">=4" +roman = "*" +sphinx = ">=7" +tabulate = ">=0.9" +tomli = {version = ">=0.10", markers = "python_version < \"3.11\""} +types-docutils = "0.22.3.20251115" + +[package.extras] +d = ["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 = "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" @@ -319,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"}, @@ -335,14 +435,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,39 +451,14 @@ 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"}, ] - -[[package]] -name = "importlib-metadata" -version = "8.7.0" -description = "Read metadata from Python packages" -optional = true -python-versions = ">=3.9" -groups = ["main"] -markers = "extra == \"docs\" and python_version == \"3.9\"" -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"}, -] - -[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"] +markers = {main = "extra == \"docs\""} [[package]] name = "iniconfig" @@ -401,14 +476,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 +491,182 @@ 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\""} + +[[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 +758,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 +840,42 @@ 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.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"] +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 +892,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 +964,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 +1061,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 +1239,72 @@ 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.2" +description = "Integer to Roman numerals converter" +optional = false +python-versions = ">=3.10" +groups = ["lint"] +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" @@ -736,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" @@ -755,6 +1330,25 @@ 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.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"] +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 +1374,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 +1392,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 +1410,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 +1444,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 +1460,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 +1478,42 @@ 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 = "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 +1558,31 @@ 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.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" +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" @@ -957,25 +1590,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.10\""} [[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\""] @@ -983,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 = true -python-versions = ">=3.9" -groups = ["main"] -markers = "extra == \"docs\" and python_version == \"3.9\"" -files = [ - {file = "zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e"}, - {file = "zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166"}, -] - -[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 0b5b86b..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", @@ -26,6 +25,13 @@ dev = [ "pytest >= 8.4.2", "pytest-cov >= 7.0.0", ] +lint = [ + "docstrfmt >= 2.0.1", + "mypy >= 1.18.2", + "ruff >= 0.6.4", + "sphinx-lint >= 1.0.0", + "types-pyyaml >=6.0.12", +] [project.optional-dependencies] docs = ["sphinx >= 7.4.7", "sphinx-rtd-theme >= 3.0.2"] @@ -49,14 +55,40 @@ 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" +# 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." +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 +109,31 @@ env.OPTS = """ --cov-branch --cov-context=test """ + +[tool.docstrfmt] +line-length = 80 + +[tool.ruff] +target-version = "py310" + +[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" diff --git a/setup.cfg b/setup.cfg index af6d9bf..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 @@ -16,3 +27,6 @@ exclude_also = [coverage:html] show_contexts = true + +[mypy] +allow_any_generics = false 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..81be841 100644 --- a/test/test_cli.py +++ b/test/test_cli.py @@ -1,13 +1,16 @@ -import confuse import argparse -from argparse import Namespace import optparse import unittest +from argparse import Namespace + +import pytest + +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 +18,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") + 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) + self.parser.add_argument("--foo", action="store_true") + self._parse("--foo") + 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): - self.config['foo'].get() + self.parser.add_argument("--foo", metavar="BAR") + self._parse("") + with pytest.raises(confuse.NotFoundError): + 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") + 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') + self.parser.add_argument("--one", dest="one.foo") + self.parser.add_argument("--two", dest="one.two.foo") + self._parse("--two TWO", dots=True) + 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') + 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) + 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') + 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) + 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) + 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 - 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') + assert self.config["one"]["two"].get() == {"foo": "TWO"} + assert 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 +89,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") + 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) + self.parser.add_option("--foo", action="store_true") + self._parse("--foo") + 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): - self.config['foo'].get() + self.parser.add_option("--foo", metavar="BAR") + self._parse("") + with pytest.raises(confuse.NotFoundError): + 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") + 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') + self.parser.add_option("--one", dest="one.foo") + self.parser.add_option("--two", dest="one.two.foo") + self._parse("--two TWO", dots=True) + 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') + 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) + 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') + 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) + assert self.config["one"]["foo"].get() == "ONE" + assert 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")) + 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') + self.config["baz"].set_args(Namespace(foo="bar")) + assert 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') + 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 743b907..875b472 100644 --- a/test/test_dump.py +++ b/test/test_dump.py @@ -1,102 +1,107 @@ -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:') + assert 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') + assert 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') + assert 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]') + assert 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(""" + assert ( + 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") + 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'}) + 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 + 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 + 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 + 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 + 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) - 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") + assert yaml == "baz: REDACTED" diff --git a/test/test_env.py b/test/test_env.py index cd8fe85..b3a9b7c 100644 --- a/test/test_env.py +++ b/test/test_env.py @@ -1,8 +1,11 @@ -import confuse import os import unittest -from . import _root +import pytest + +import confuse + +from . import _root ENVIRON = os.environ @@ -15,233 +18,229 @@ 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_")) + assert 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() - self.assertIsInstance(foo, int) - self.assertEqual(foo, 1) - self.assertIsInstance(bar, float) - self.assertEqual(bar, 2.0) + os.environ["TEST_FOO"] = "1" + os.environ["TEST_BAR"] = "2.0" + config = _root(confuse.EnvSource("TEST_")) + foo = config["foo"].get() + bar = config["bar"].get() + 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) + os.environ["TEST_FOO"] = "true" + os.environ["TEST_BAR"] = "FALSE" + config = _root(confuse.EnvSource("TEST_")) + 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) + os.environ["TEST_FOO"] = "null" + os.environ["TEST_BAR"] = "" + config = _root(confuse.EnvSource("TEST_")) + 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') - config.set(confuse.EnvSource('TEST_')) - self.assertIs(config['foo'].get(), None) + os.environ["TEST_FOO"] = "null" + config = _root({"foo": "bar"}) + assert config["foo"].get() == "bar" + config.set(confuse.EnvSource("TEST_")) + 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') + os.environ["TEST_FOO__BAR"] = "a" + os.environ["TEST_FOO_BAZ"] = "b" + config = _root(confuse.EnvSource("TEST_")) + 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') + os.environ["TEST_FOO__BAR"] = "a" + os.environ["TEST_FOO_BAZ"] = "b" + config = _root(confuse.EnvSource("TEST_", sep="_")) + 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') + os.environ["TEST_FOO__BAR"] = "a" + os.environ["TEST_FOO__BAZ__QUX"] = "b" + config = _root(confuse.EnvSource("TEST_")) + 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') + os.environ["TEST_FOO__BAZ__QUX"] = "b" + os.environ["TEST_FOO__BAR"] = "a" + config = _root(confuse.EnvSource("TEST_")) + 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_')) + 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 - 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') + 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(), {}) + os.environ["TEST_FOO"] = "a" + config = _root(confuse.EnvSource("test_", lower=True)) + 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'}) + os.environ["TEST_foo"] = "a" + config = _root(confuse.EnvSource("TEST_", lower=True)) + 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'}) + os.environ["TEST_FOO"] = "UPPER" + config = _root(confuse.EnvSource("TEST_", lower=True)) + 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'}) + os.environ["TEST_FOO"] = "a" + config = _root(confuse.EnvSource("TEST_", lower=False)) + 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']) + 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)) + assert 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)) + 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']]) + 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)) + 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'}) + 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)) + 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'}) + 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)) + 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'}) + 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)) + 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'}) + os.environ["TEST_0"] = "a" + os.environ["TEST_1"] = "b" + os.environ["TEST_2"] = "c" + config = _root(confuse.EnvSource("TEST_", handle_lists=True)) + 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'}) + os.environ["TEST_FOO__BAR"] = "a" + os.environ["TEST_FOO__BAZ"] = "b" + config = _root(confuse.EnvSource("TEST_", handle_lists=True)) + 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') + os.environ["TEST_FOO"] = "a" + config = _root(confuse.EnvSource("TEST_", parse_yaml_docs=True)) + 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']) + os.environ["TEST_FOO"] = "[a, b]" + config = _root(confuse.EnvSource("TEST_", parse_yaml_docs=True)) + 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'}) + os.environ["TEST_FOO"] = "{bar: a, baz: b}" + config = _root(confuse.EnvSource("TEST_", parse_yaml_docs=True)) + 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'}) + os.environ["TEST_FOO"] = "{bar: [a, b], baz: {qux: c}}" + config = _root(confuse.EnvSource("TEST_", parse_yaml_docs=True)) + 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) + 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() + 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) + os.environ["TEST_FOO"] = "{bar: true, baz: FALSE}" + config = _root(confuse.EnvSource("TEST_", parse_yaml_docs=True)) + 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) + os.environ["TEST_FOO"] = "{bar: null, baz: }" + config = _root(confuse.EnvSource("TEST_", parse_yaml_docs=True)) + 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: - _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') + os.environ["TEST_FOO"] = "{:}" + with pytest.raises(confuse.ConfigError, match="TEST_FOO"): + _root(confuse.EnvSource("TEST_", parse_yaml_docs=True)) 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): - config['foo']['bar'].get() + os.environ["TEST_FOO"] = "{bar: a, baz: b}" + config = _root(confuse.EnvSource("TEST_", parse_yaml_docs=False)) + assert config["foo"].get() == "{bar: a, baz: b}" + with pytest.raises(confuse.ConfigError): + 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 +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'}) + self.config.set_env(prefix="MYAPP_") + 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'}}) + self.config.set_env(sep="_sep_") + 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'}}) + self.config.set_env(prefix="MYAPP_", sep="_sep_") + assert self.config.get() == {"qux": {"nested": "d"}} diff --git a/test/test_paths.py b/test/test_paths.py index c832b5e..d31bf44 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 @@ -7,108 +5,124 @@ import shutil import tempfile import unittest +from typing import ClassVar +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): - 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 + + def tearDown(self): + super().tearDown() + shutil.rmtree(self.home) - if self.SYS_NAME in SYSTEMS: - 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 +class FakeSystem(unittest.TestCase): + SYS_NAME: ClassVar[str] + + def setUp(self): + super().setUp() + self.os_path = os.path + os.environ = {} - if self.TMP_HOME: - os.environ['HOME'] = self.home - os.environ['USERPROFILE'] = self.home + 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): - 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']) + 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']) + del os.environ["XDG_CONFIG_HOME"] + 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']) + os.environ["XDG_CONFIG_HOME"] = "~/.config" + 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']) + os.environ["XDG_CONFIG_DIRS"] = "/usr/local/etc/xdg:/etc/xdg" + assert 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']) + 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']) + os.environ["XDG_CONFIG_DIRS"] = "/usr/local/etc/xdg:/etc/xdg" + 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' + 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']) + del os.environ["APPDATA"] + assert confuse.config_dirs() == ["C:\\Users\\test\\AppData\\Roaming"] class ConfigFilenamesTest(unittest.TestCase): @@ -121,12 +135,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, []) + assert 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: @@ -136,81 +150,59 @@ 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 +class EnvVarTest(FakeHome): 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 + 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) + del os.environ["MYAPPDIR"] + assert self.config.config_dir() != self.home -class PrimaryConfigDirTest(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 +@unittest.skipUnless(os.system == "Linux", "Linux-specific tests") +class PrimaryConfigDirTest(FakeHome, FakeSystem): + SYS_NAME = "Linux" # conversion from posix to nt is easy def setUp(self): - super(PrimaryConfigDirTest, self).setUp() - if hasattr(self, 'join'): - os.path.join = self.join - os.makedirs, self._makedirs = self.makedirs, os.makedirs + super().setUp() - self.config = confuse.Configuration('test', read=False) - - def tearDown(self): - super(PrimaryConfigDirTest, self).tearDown() - if hasattr(self, '_makedirs'): - os.makedirs = self._makedirs + self.config = confuse.Configuration("test", read=False) 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)) + 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') + 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') - 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) 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 b8ebc9c..94a3387 100644 --- a/test/test_utils.py +++ b/test/test_utils.py @@ -1,75 +1,78 @@ +import unittest from argparse import Namespace from collections import OrderedDict + +import pytest + 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']) + 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} + 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']) + result = confuse.util.build_dict(config.copy(), sep=".") + assert 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=".") + 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']) + result = confuse.util.build_dict(config.copy(), sep=".") + 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)] + 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=".") + 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} + 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']) + result = confuse.util.build_dict(config.copy(), sep="_") + assert 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="__") + assert 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'] + 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}} + 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 701cd68..fad61a6 100644 --- a/test/test_valid.py +++ b/test/test_valid.py @@ -1,696 +1,702 @@ -from collections.abc import Mapping, Sequence -import confuse import enum import os import unittest +from collections.abc import Mapping, Sequence + +import pytest + +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()}) + assert 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)}) + 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): - valid['bar'] + config = _root({"foo": 5}) + valid = config.get({"foo": confuse.Integer()}) + 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): - valid['bar'] + config = _root({"foo": 5, "bar": 6}) + valid = config.get({"foo": confuse.Integer()}) + 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) + config = _root({"foo": 5}) + valid = config.get({"foo": int}) + assert 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}) + assert valid["foo"] == 9 def test_attribute_access(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()}) + assert 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()}) + 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']) + valid = config.get({"foo": confuse.Integer(None)}) + assert valid["foo"] is None def test_wrong_type_raises_error_on_validate(self): - config = _root({'foo': 'bar'}) - with self.assertRaises(confuse.ConfigTypeError): - config.get({'foo': confuse.Integer()}) + config = _root({"foo": "bar"}) + 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) + config = _root({"foo": 5}) + valid = config["foo"].get(confuse.Integer()) + assert 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()}, + } + ) + assert valid["foo"]["bar"] == 9 def test_nested_attribute_access(self): - config = _root({ - 'foo': {'bar': 8}, - }) - valid = config.get({ - 'foo': {'bar': confuse.Integer()}, - }) - self.assertEqual(valid.foo.bar, 8) + config = _root( + { + "foo": {"bar": 8}, + } + ) + valid = config.get( + { + "foo": {"bar": confuse.Integer()}, + } + ) + 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') + typ = confuse.as_template("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) + typ = confuse.as_template({"key": 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(typ.subtemplates['outer'].subtemplates['inner'], - confuse.Integer) - self.assertEqual(typ.subtemplates['outer'].subtemplates['inner'] - .default, 2) + typ = confuse.as_template({"outer": {"inner": 2}}) + assert isinstance(typ, confuse.MappingTemplate) + assert isinstance(typ.subtemplates["outer"], confuse.MappingTemplate) + assert isinstance( + typ.subtemplates["outer"].subtemplates["inner"], confuse.Integer + ) + 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.) - self.assertIsInstance(typ, confuse.Number) - self.assertEqual(typ.default, 2.) + typ = confuse.as_template(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(): + 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') + config = _root({"foo": "bar"}) + valid = config.get({"foo": confuse.String()}) + assert 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")}) + 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): - config.get({'baz': confuse.String(pattern='!')}) + config = _root({"foo": "bar", "baz": "zab"}) + valid = config.get({"foo": confuse.String(pattern="^ba.$")}) + 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') + config = _root({"foo": "bar"}) + valid = config.get({"foo": str}) + assert 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"}) + assert valid["foo"] == "bar" def test_check_string_type(self): - config = _root({'foo': 5}) - with self.assertRaises(confuse.ConfigTypeError): - config.get({'foo': confuse.String()}) + config = _root({"foo": 5}) + with pytest.raises(confuse.ConfigTypeError): + 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()) - self.assertIsInstance(valid, int) - self.assertEqual(valid, 2) + config = _root({"foo": 2}) + valid = config["foo"].get(confuse.Number()) + 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) + config = _root({"foo": 3.0}) + valid = config["foo"].get(confuse.Number()) + assert isinstance(valid, float) + assert valid == 3.0 def test_validate_string_as_number(self): - config = _root({'foo': 'bar'}) - with self.assertRaises(confuse.ConfigTypeError): - config['foo'].get(confuse.Number()) + config = _root({"foo": "bar"}) + with pytest.raises(confuse.ConfigTypeError): + 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])) - self.assertEqual(valid, 2) + config = _root({"foo": 2}) + valid = config["foo"].get(confuse.Choice([1, 2, 4, 8, 16])) + assert valid == 2 def test_validate_bad_choice_in_list(self): - config = _root({'foo': 3}) - with self.assertRaises(confuse.ConfigValueError): - config['foo'].get(confuse.Choice([1, 2, 4, 8, 16])) + config = _root({"foo": 3}) + 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') + config = _root({"foo": 2}) + valid = config["foo"].get(confuse.Choice({2: "two", 4: "four"})) + assert valid == "two" def test_validate_bad_choice_in_dict(self): - config = _root({'foo': 3}) - with self.assertRaises(confuse.ConfigValueError): - config['foo'].get(confuse.Choice({2: 'two', 4: 'four'})) + config = _root({"foo": 3}) + with pytest.raises(confuse.ConfigValueError): + 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")) + assert valid == "bar" def test_validate_good_choice_in_list(self): - config = _root({'foo': 2}) - valid = config['foo'].get(confuse.OneOf([ - confuse.String(), - confuse.Integer(), - ])) - self.assertEqual(valid, 2) + config = _root({"foo": 2}) + valid = config["foo"].get( + confuse.OneOf( + [ + confuse.String(), + confuse.Integer(), + ] + ) + ) + assert 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(), - ])) - self.assertEqual(valid, 3) + config = _root({"foo": 3.14}) + valid = config["foo"].get( + confuse.OneOf( + [ + confuse.Integer(), + confuse.Number(), + ] + ) + ) + assert valid == 3 def test_validate_no_choice_in_list(self): - config = _root({'foo': None}) - with self.assertRaises(confuse.ConfigValueError): - config['foo'].get(confuse.OneOf([ - confuse.String(), - confuse.Integer(), - ])) + config = _root({"foo": None}) + with pytest.raises(confuse.ConfigValueError): + 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): + with pytest.raises(confuse.ConfigTemplateError): config.get(confuse.OneOf([BadTemplate()])) del 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()) + 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']) + config = _root({"foo": ("bar", "baz")}) + valid = config["foo"].get(confuse.StrSeq()) + 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']) + config = _root({"foo": "bar baz"}) + valid = config["foo"].get(confuse.StrSeq()) + assert valid == ["bar", "baz"] def test_invalid_type(self): - config = _root({'foo': 9}) - with self.assertRaises(confuse.ConfigTypeError): - config['foo'].get(confuse.StrSeq()) + config = _root({"foo": 9}) + 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): - config['foo'].get(confuse.StrSeq()) + config = _root({"foo": ["bar", 2126]}) + with pytest.raises(confuse.ConfigTypeError): + 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")) + assert valid == "foo/bar" def test_default_none(self): config = _root({}) - valid = config['foo'].get(confuse.Filename(None)) - self.assertEqual(valid, None) + valid = config["foo"].get(confuse.Filename(None)) + assert valid is None def test_missing_required_value(self): config = _root({}) - with self.assertRaises(confuse.NotFoundError): - config['foo'].get(confuse.Filename()) + 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')) + config = _root({"foo": "bar"}) + valid = config["foo"].get(confuse.Filename(cwd="/dev/null")) + 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')) + config = _root({"foo": "/", "bar": "baz"}) + valid = config.get( + {"foo": confuse.Filename(), "bar": confuse.Filename(relative_to="foo")} + ) + 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')) + config = _root({"foo": "bar"}) + valid = config.get( + {"foo": confuse.Filename(cwd="/dev/null", relative_to="baz")} + ) + 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): - config.get({ - 'foo': confuse.Filename(relative_to='bar'), - 'bar': confuse.Filename(relative_to='baz'), - 'baz': confuse.Filename(relative_to='foo') - }) + config = _root({"foo": "/", "bar": "r", "baz": "z"}) + with pytest.raises(confuse.ConfigTemplateError): + 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'}) - with self.assertRaises(confuse.ConfigTemplateError): - config.get({ - 'foo': confuse.Filename(relative_to='foo') - }) + config = _root({"foo": "bar"}) + 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): - config['foo'].get(confuse.Filename(relative_to='bar')) + config = _root({"foo": "bar"}) + 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): - config.get({ - 'bar': confuse.Filename(relative_to='foo') - }) + config = _root({"foo": "/", "bar": "baz"}) + 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')) + config = _root({"foo": "foo/bar"}) + valid = config["foo"].get(confuse.Filename()) + 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') + 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()) + assert 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()) + assert 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()) + 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') + 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)) + 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') + 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)) + assert 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)) + assert 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)) + assert valid == os.path.realpath("/config/path/foo/bar") def test_filename_wrong_type(self): - config = _root({'foo': 8}) - with self.assertRaises(confuse.ConfigTypeError): - config['foo'].get(confuse.Filename()) + config = _root({"foo": 8}) + with pytest.raises(confuse.ConfigTypeError): + 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()) + 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')) + valid = config["foo"].get(confuse.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) + valid = config["foo"].get(confuse.Path(None)) + assert valid is None def test_missing_required_value(self): config = _root({}) - with self.assertRaises(confuse.NotFoundError): - config['foo'].get(confuse.Path()) + with pytest.raises(confuse.NotFoundError): + 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()) - self.assertEqual(valid, 4.2) + config = _root({"foo": 4.2}) + valid = config["foo"].get(confuse.Template()) + assert valid == 4.2 def test_base_template_required(self): config = _root({}) - with self.assertRaises(confuse.NotFoundError): - config['foo'].get(confuse.Template()) + 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') + valid = config["foo"].get(confuse.Template("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()) + config = _root({"foo": set()}) + valid = config["foo"].get(confuse.TypeTemplate(set)) + assert valid == set() def test_incorrect_type(self): - config = _root({'foo': dict()}) - with self.assertRaises(confuse.ConfigTypeError): - config['foo'].get(confuse.TypeTemplate(set)) + config = _root({"foo": dict()}) + with pytest.raises(confuse.ConfigTypeError): + 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)) + 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]) + config = _root({"foo": [1, 2, 3]}) + valid = config["foo"].get(confuse.Sequence(int)) + 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} - ]) + config = _root({"foo": [{"bar": 1, "baz": 2}, {"bar": 3, "baz": 4}]}) + valid = config["foo"].get(confuse.Sequence({"bar": int, "baz": int})) + 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): - config['foo'].get(confuse.Sequence( - {'bar': int, 'baz': int} - )) + config = _root({"foo": [{"bar": 1, "baz": 2}, {"bar": 3, "bak": 4}]}) + 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): - config['foo'].get(confuse.Sequence(int)) + config = _root({"foo": {"one": 1, "two": 2, "three": 3}}) + 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, []) + config = _root({"foo": [1, 2, 3]}) + valid = config["bar"].get(confuse.Sequence(int)) + 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}) + config = _root({"foo": {"one": 1, "two": 2, "three": 3}}) + valid = config["foo"].get(confuse.MappingValues(int)) + 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}, - }) + config = _root( + {"foo": {"first": {"bar": 1, "baz": 2}, "second": {"bar": 3, "baz": 4}}} + ) + valid = config["foo"].get(confuse.MappingValues({"bar": int, "baz": int})) + 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): - config['foo'].get(confuse.MappingValues( - {'bar': int, 'baz': int} - )) + config = _root( + {"foo": {"first": {"bar": 1, "baz": 2}, "second": {"bar": 3, "bak": 4}}} + ) + 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): - config['foo'].get(confuse.MappingValues(int)) + config = _root({"foo": [1, 2, 3]}) + 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, {}) + config = _root({"foo": {"one": 1, "two": 2, "three": 3}}) + valid = config["bar"].get(confuse.MappingValues(int)) + 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') + config = _root({"foo": "bar"}) + valid = config["foo"].get(confuse.Optional(confuse.String())) + assert valid == "bar" def test_optional_string_invalid_type(self): - config = _root({'foo': 5}) - with self.assertRaises(confuse.ConfigTypeError): - config['foo'].get(confuse.Optional(confuse.String())) + config = _root({"foo": 5}) + 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) + config = _root({"foo": None}) + valid = config["foo"].get(confuse.Optional(confuse.String())) + 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') + config = _root({"foo": None}) + valid = config["foo"].get(confuse.Optional(confuse.String(), "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') + config = _root({"foo": None}) + valid = config["foo"].get(confuse.Optional(confuse.String("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') + config = _root({"foo": None}) + valid = config["foo"].get( + confuse.Optional(confuse.String("baz"), default="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) + valid = config["foo"].get(confuse.Optional(confuse.String())) + 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') + valid = config["foo"].get(confuse.Optional(confuse.String(), "baz")) + assert 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) - ) + with pytest.raises(confuse.NotFoundError): + 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) + 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') + config = _root({"foo": {"bar": 5, "baz": "bak"}}) + template = {"bar": confuse.Integer(), "baz": confuse.String()} + valid = config.get({"foo": confuse.Optional(template)}) + 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): - config.get({'foo': confuse.Optional(template)}) + config = _root({"foo": {"bar": 5, "baz": 10}}) + template = {"bar": confuse.Integer(), "baz": confuse.String()} + 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']) + config = _root({"foo": None}) + template = {"bar": confuse.Integer(), "baz": confuse.String()} + valid = config.get({"foo": confuse.Optional(template)}) + 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) + config = _root({"foo": None}) + template = {"bar": confuse.Integer(), "baz": confuse.String()} + valid = config.get({"foo": confuse.Optional(template, {})}) + 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']) + template = {"bar": confuse.Integer(), "baz": confuse.String()} + valid = config.get({"foo": confuse.Optional(template)}) + 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) + template = {"bar": confuse.Integer(), "baz": confuse.String()} + valid = config.get({"foo": confuse.Optional(template, {})}) + 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): - config.get({'foo': confuse.Optional(template, - allow_missing=False)}) + template = {"bar": confuse.Integer(), "baz": confuse.String()} + 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']) + config = _root({"foo": None}) + template = {"bar": confuse.Integer(), "baz": confuse.String()} + valid = config.get({"foo": confuse.Optional(template, allow_missing=False)}) + assert valid["foo"] is None diff --git a/test/test_validation.py b/test/test_validation.py index bb212f5..793af12 100644 --- a/test/test_validation.py +++ b/test/test_validation.py @@ -1,151 +1,147 @@ -import confuse import enum import os import unittest + +import pytest + +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) + assert value == "bar" def test_str_type_incorrect(self): - config = _root({'foo': 2}) - with self.assertRaises(confuse.ConfigTypeError): - config['foo'].get(str) + config = _root({"foo": 2}) + 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) + config = _root({"foo": 2}) + value = config["foo"].get(int) + assert value == 2 def test_int_type_incorrect(self): - config = _root({'foo': 'bar'}) - with self.assertRaises(confuse.ConfigTypeError): - config['foo'].get(int) + config = _root({"foo": "bar"}) + with pytest.raises(confuse.ConfigTypeError): + 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() + 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') + 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() + assert 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() + 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): - config['foo'].as_filename() + config = _root({"foo": None}) + with pytest.raises(confuse.ConfigTypeError): + 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() + with pytest.raises(ImportError): + value = config["foo"].as_path() else: - value = config['foo'].as_path() + 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') + config = _root({"foo": "bar"}) + value = config["foo"].as_choice(["foo", "bar", "baz"]) + assert value == "bar" def test_as_choice_error(self): - config = _root({'foo': 'bar'}) - with self.assertRaises(confuse.ConfigValueError): - config['foo'].as_choice(['foo', 'baz']) + config = _root({"foo": "bar"}) + with pytest.raises(confuse.ConfigValueError): + 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", + } + ) + assert 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) - self.assertEqual(res, Foobar.Foo) + config = _root({"foo": Foobar.Foo.value}) + res = config["foo"].as_choice(Foobar) + assert res == Foobar.Foo def test_as_choice_with_enum_error(self): class Foobar(enum.Enum): - Foo = 'bar' + Foo = "bar" - config = _root({'foo': 'foo'}) - with self.assertRaises(confuse.ConfigValueError): - config['foo'].as_choice(Foobar) + config = _root({"foo": "foo"}) + with pytest.raises(confuse.ConfigValueError): + 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'}) - with self.assertRaises(confuse.ConfigTypeError): - config['s'].as_number() + config = _root({"s": "a"}) + 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'] - ) + config = _root({"k": "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'] - ) + config = _root({"k": ["a b", "c"]}) + assert 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}) - with self.assertRaises(confuse.ConfigTypeError): - config['f'].as_str() + config = _root({"f": 1.0}) + 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') + config = _root({"s": "${CONFUSE_TEST_VAR}/bar"}) + os.environ["CONFUSE_TEST_VAR"] = "foo" + 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() - ) + config = _root({"k": [{"a": "A"}, "b", ["c", "C"]]}) + assert [("a", "A"), ("b", None), ("c", "C")] == config["k"].as_pairs() diff --git a/test/test_views.py b/test/test_views.py index 32f73ca..ddd825e 100644 --- a/test/test_views.py +++ b/test/test_views.py @@ -1,263 +1,267 @@ -import confuse import unittest + +import pytest + +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() + assert 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() + assert value == "baz" def test_missing_key(self): - config = _root({'foo': 'bar'}) - with self.assertRaises(confuse.NotFoundError): - config['baz'].get() + config = _root({"foo": "bar"}) + with pytest.raises(confuse.NotFoundError): + config["baz"].get() def test_missing_index(self): - config = _root({'l': ['foo', 'bar']}) - with self.assertRaises(confuse.NotFoundError): - config['l'][5].get() + config = _root({"l": ["foo", "bar"]}) + with pytest.raises(confuse.NotFoundError): + 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'])) + 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']) + config = _root({"l": ["foo", "bar"]}) + items = [subview.get() for subview in config["l"]] + assert items == ["foo", "bar"] def test_int_iter(self): - config = _root({'n': 2}) - with self.assertRaises(confuse.ConfigTypeError): - [item for item in config['n']] + config = _root({"n": 2}) + with pytest.raises(confuse.ConfigTypeError): + [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'])) + assert set(keys) == {"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'])) + assert set(values) == {"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')])) + assert set(items) == {("foo", "bar"), ("baz", "qux")} def test_list_keys_error(self): - config = _root({'l': ['foo', 'bar']}) - with self.assertRaises(confuse.ConfigTypeError): - config['l'].keys() + config = _root({"l": ["foo", "bar"]}) + 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']) + config = _root({"l": ["foo", "bar"]}) + items = [item.get() for item in config["l"].sequence()] + assert items == ["foo", "bar"] def test_dict_sequence_error(self): - config = _root({'foo': 'bar', 'baz': 'qux'}) - with self.assertRaises(confuse.ConfigTypeError): + config = _root({"foo": "bar", "baz": "qux"}) + with pytest.raises(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'])) + 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']) + config = _root({"l": ["foo", "bar"]}) + contents = config["l"].all_contents() + assert list(contents) == ["foo", "bar"] def test_int_contents(self): - config = _root({'n': 2}) - with self.assertRaises(confuse.ConfigTypeError): - list(config['n'].all_contents()) + config = _root({"n": 2}) + with pytest.raises(confuse.ConfigTypeError): + 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"]) + assert 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"]) + assert value == "2" def test_bool_conversion_from_bool(self): - config = _root({'foo': True}) - value = bool(config['foo']) - self.assertEqual(value, True) + config = _root({"foo": True}) + value = bool(config["foo"]) + assert value def test_bool_conversion_from_int(self): - config = _root({'foo': 0}) - value = bool(config['foo']) - self.assertEqual(value, False) + config = _root({"foo": 0}) + value = bool(config["foo"]) + 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") + name = config["foo"].name + 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") + name = config[5]["foo"]["bar"][20].name + 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') + config = _root({"foo": "bar"}, {"foo": "baz"}) + value = config["foo"].get() + assert 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() + assert value == "baz" def test_dict_access_missing(self): - config = _root({'qux': 'bar'}, {'foo': 'baz'}) - with self.assertRaises(confuse.NotFoundError): - config['fred'].get() + config = _root({"qux": "bar"}, {"foo": "baz"}) + 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') + config = _root({"l": ["a", "b"]}, {"l": ["c", "d", "e"]}) + value = config["l"][1].get() + 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') + config = _root({"l": ["a", "b"]}, {"l": ["c", "d", "e"]}) + value = config["l"][2].get() + assert value == "e" def test_list_access_missing(self): - config = _root({'l': ['a', 'b']}, {'l': ['c', 'd', 'e']}) - with self.assertRaises(confuse.NotFoundError): - config['l'][3].get() + config = _root({"l": ["a", "b"]}, {"l": ["c", "d", "e"]}) + 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'}) + config = _root({"foo": {"bar": "baz"}}, {"foo": {"qux": "fred"}}) + value = config["foo"].get() + 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'])) + config = _root({"foo": {"bar": "baz"}}, {"foo": {"qux": "fred"}}) + keys = config["foo"].keys() + 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']) + config = _root({"foo": {"bar": "baz"}}, {"foo": {"bar": "fred"}}) + keys = config["foo"].keys() + 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'])) + config = _root({"foo": {"bar": "baz"}}, {"foo": {"qux": "fred"}}) + values = [value.get() for value in config["foo"].values()] + 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']) + config = _root({"foo": {"bar": "baz"}}, {"foo": {"bar": "fred"}}) + values = [value.get() for value in config["foo"].values()] + 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')])) + config = _root({"foo": {"bar": "baz"}}, {"foo": {"qux": "fred"}}) + items = [(key, value.get()) for (key, value) in config["foo"].items()] + 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')]) + config = _root({"foo": {"bar": "baz"}}, {"foo": {"bar": "fred"}}) + items = [(key, value.get()) for (key, value) in config["foo"].items()] + 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']) + config = _root({"l": ["a", "b"]}, {"l": ["c", "d", "e"]}) + items = [item.get() for item in config["l"].sequence()] + 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): - list(config['foo'].sequence()) + config = _root({"foo": {"bar": "baz"}}, {"foo": ["qux", "fred"]}) + 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'])) + config = _root({"foo": {"bar": "baz"}}, {"foo": {"qux": "fred"}}) + contents = config["foo"].all_contents() + 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']) + config = _root({"foo": {"bar": "baz"}}, {"foo": {"bar": "fred"}}) + contents = config["foo"].all_contents() + 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']) + config = _root({"foo": ["bar", "baz"]}, {"foo": ["qux", "fred"]}) + contents = config["foo"].all_contents() + 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): - list(config['foo'].all_contents()) + config = _root({"foo": ["bar", "baz"]}, {"foo": 5}) + 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']) + config = _root({"foo": ["bar", "baz"]}, {"foo": {"qux": "fred"}}) + contents = config["foo"].all_contents() + 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') + config = _root({"foo": "bar"}) + config.add({"baz": "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') + config["foo"] = "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') + config = _root({"foo": "bar"}) + config["foo"] = "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') + config["foo"]["bar"] = "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') + config = _root({"foo": {"bar": "qux"}}) + config["foo"]["bar"] = "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') + config = _root({"foo": ["a", "b", "c"]}) + config["foo"][1] = "bar" + assert config["foo"][1].get() == "bar" diff --git a/test/test_yaml.py b/test/test_yaml.py index ccb6b30..ea30409 100644 --- a/test/test_yaml.py +++ b/test/test_yaml.py @@ -1,6 +1,10 @@ -import confuse -import yaml import unittest + +import pytest +import yaml + +import confuse + from . import TempDir @@ -11,117 +15,101 @@ 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): 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") + assert v["foo"] == "bar" def test_syntax_error(self): - try: - self._parse_contents(b':') - except confuse.ConfigError as exc: - self.assertTrue('test_config.yaml' in exc.name) - else: - self.fail('ConfigError not raised') + with pytest.raises(confuse.ConfigError, match=r"test_config\.yaml"): + self._parse_contents(b":") 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') + 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') + v = confuse.load_yaml_string("foo: bar", "test") + assert v["foo"] == "bar" def test_string_syntax_error(self): - try: - confuse.load_yaml_string(':', 'test') - except confuse.ConfigError as exc: - self.assertTrue('test' in exc.name) - else: - self.fail('ConfigError not raised') + with pytest.raises(confuse.ConfigError, match="test"): + confuse.load_yaml_string(":", "test") def test_string_tab_indentation_error(self): - try: - 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') + with pytest.raises(confuse.ConfigError, match="found tab"): + confuse.load_yaml_string("foo:\n\tbar: baz", "test") 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) + 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) + v = confuse.yaml_util.parse_as_scalar("1", confuse.Loader) + 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) + v = confuse.yaml_util.parse_as_scalar("1.0", confuse.Loader) + 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) + v = confuse.yaml_util.parse_as_scalar("true", confuse.Loader) + assert v is True def test_empty_string_to_none(self): - v = confuse.yaml_util.parse_as_scalar('', confuse.Loader) - self.assertIs(v, None) + v = confuse.yaml_util.parse_as_scalar("", confuse.Loader) + 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) + v = confuse.yaml_util.parse_as_scalar("null", confuse.Loader) + 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'}) + v = confuse.yaml_util.parse_as_scalar({"foo": "bar"}, confuse.Loader) + 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']) + v = confuse.yaml_util.parse_as_scalar(["foo", "bar"], confuse.Loader) + assert 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) + assert v == "!"