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 == "!"