diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml deleted file mode 100644 index b6942e9..0000000 --- a/.github/workflows/deploy.yml +++ /dev/null @@ -1,63 +0,0 @@ -name: Deploy - -on: - push: - branches: - - master - - "*deploy*" - release: - types: - - published - -jobs: - build: - if: github.repository == 'pytest-dev/iniconfig' - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: Cache - uses: actions/cache@v4 - with: - path: ~/.cache/pip - key: deploy-${{ hashFiles('**/pyproject.toml') }} - restore-keys: | - deploy- - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.x" - - - name: Install build + twine - run: python -m pip install build twine setuptools_scm - - - name: git describe output - run: git describe --tags - - - id: scm_version - run: | - VERSION=$(python -m setuptools_scm --strip-dev) - echo SETUPTOOLS_SCM_PRETEND_VERSION=$VERSION >> $GITHUB_ENV - - - name: Build package - run: python -m build - - - name: twine check - run: twine check dist/* - - - name: Publish package to PyPI - if: github.event.action == 'published' - uses: pypa/gh-action-pypi-publish@release/v1 - with: - user: __token__ - password: ${{ secrets.pypi_password }} - - - name: Publish package to TestPyPI - uses: pypa/gh-action-pypi-publish@release/v1 - with: - user: __token__ - password: ${{ secrets.test_pypi_password }} - repository_url: https://test.pypi.org/legacy/ diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml deleted file mode 100644 index f491e17..0000000 --- a/.github/workflows/main.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: build - -on: [push, pull_request, workflow_dispatch] - -jobs: - build: - - runs-on: ${{ matrix.os }} - - strategy: - fail-fast: false - matrix: - python: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] - os: [ubuntu-latest, windows-latest] - - steps: - - uses: actions/checkout@v4 - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python }} - allow-prereleases: true - - name: Install hatch - run: python -m pip install --upgrade pip hatch hatch-vcs - - name: Run tests - run: hatch run +py=${{ matrix.python }} test:default --color=yes - - pre-commit: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: 3.x - - uses: pre-commit/action@v3.0.1 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..8368741 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,144 @@ +name: test + +on: [push, pull_request, workflow_dispatch] + +jobs: + build-and-inspect: + runs-on: ubuntu-latest + permissions: + id-token: write + attestations: write + contents: read + outputs: + python-versions: ${{ steps.baipp.outputs.supported_python_classifiers_json_array }} + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Build and inspect package + id: baipp + uses: hynek/build-and-inspect-python-package@v2 + with: + attest-build-provenance-github: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} + env: + SETUPTOOLS_SCM_OVERRIDES_FOR_INICONFIG: ${{ github.ref == 'refs/heads/main' && 'local_scheme="no-local-version"' || '' }} + + test: + needs: build-and-inspect + runs-on: ${{ matrix.os }} + defaults: + run: + shell: bash + + strategy: + fail-fast: false + matrix: + python: ${{ fromJSON(needs.build-and-inspect.outputs.python-versions) }} + os: [ubuntu-latest, windows-latest] + + steps: + - name: Checkout test files + uses: actions/checkout@v4 + with: + sparse-checkout: | + testing + uv.lock + sparse-checkout-cone-mode: false + + - name: Download built packages + uses: actions/download-artifact@v4 + with: + name: Packages + path: dist + + - name: Verify artifacts exist + run: | + ls -la dist/ + test -f dist/*.whl || (echo "No wheel found!" && exit 1) + test -f dist/*.tar.gz || (echo "No sdist found!" && exit 1) + + - name: Install uv + uses: astral-sh/setup-uv@v7 + with: + enable-cache: true + + - name: Set up Python ${{ matrix.python }} + run: uv python install ${{ matrix.python }} + + - name: Run tests with built wheel + run: uv run --with dist/*.whl --with pytest pytest testing --color=yes + + create-release: + name: Create GitHub Release + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') + needs: [test, build-and-inspect] + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Download built packages + uses: actions/download-artifact@v4 + with: + name: Packages + path: dist + + - name: Extract version from tag + id: version + run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + files: dist/* + fail_on_unmatched_files: true + generate_release_notes: true + name: Version ${{ steps.version.outputs.VERSION }} + + publish-to-pypi: + name: Publish to PyPI + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') + needs: [test, build-and-inspect, create-release] + runs-on: ubuntu-latest + environment: pypi-upload + permissions: + id-token: write + + steps: + - name: Download built packages + uses: actions/download-artifact@v4 + with: + name: Packages + path: dist + + - name: Publish package to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + + publish-to-test-pypi: + name: Publish to TestPyPI + if: | + github.repository == 'pytest-dev/iniconfig' && + (github.event_name == 'push' && ( + github.ref == 'refs/heads/main' || + contains(github.ref, 'deploy') + )) + needs: [test, build-and-inspect] + runs-on: ubuntu-latest + environment: test-pypi-upload + permissions: + id-token: write + attestations: write + + steps: + - name: Download built packages + uses: actions/download-artifact@v4 + with: + name: Packages + path: dist + + - name: Publish package to TestPyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + repository-url: https://test.pypi.org/legacy/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 20e04ba..1e54504 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,24 +1,21 @@ repos: -- repo: https://github.com/asottile/pyupgrade - rev: v3.3.1 - hooks: - - id: pyupgrade - args: [--py38-plus] - repo: https://github.com/tox-dev/pyproject-fmt - rev: "0.4.1" + rev: "v2.11.0" hooks: - id: pyproject-fmt -- repo: https://github.com/psf/black - rev: 22.12.0 +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.14.1 hooks: - - id: black - language_version: python3 -- repo: https://github.com/pre-commit/mirrors-mypy - rev: 'v0.991' - hooks: - - id: mypy - args: [] - additional_dependencies: - - "pytest==7.2.0" - - "tomli" \ No newline at end of file + - id: ruff-check + args: [--fix] + - id: ruff-format + +- repo: https://github.com/pre-commit/mirrors-mypy + rev: 'v1.18.2' + hooks: + - id: mypy + args: [] + additional_dependencies: + - "pytest==7.2.0" + - "tomli" \ No newline at end of file diff --git a/CHANGELOG b/CHANGELOG index cc3a51a..eeb0245 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,17 @@ +2.2.0 +===== + +* drop Python 3.8 and 3.9 support (now requires Python >= 3.10) +* add Python 3.14 classifier +* migrate from hatchling to setuptools 77 with setuptools_scm +* adopt PEP 639 license specifiers and PEP 740 build attestations +* migrate from black + pyupgrade to ruff +* migrate CI to uv and unified test workflow +* automate GitHub releases and PyPI publishing via Trusted Publishing +* include tests in sdist +* modernize code for Python 3.10+ (remove __future__ annotations, TYPE_CHECKING guards) +* rename _ParsedLine to ParsedLine + 2.1.0 ===== diff --git a/pyproject.toml b/pyproject.toml index 78dcdc8..50db162 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,8 +1,8 @@ [build-system] -build-backend = "hatchling.build" +build-backend = "setuptools.build_meta" requires = [ - "hatch-vcs", - "hatchling>=1.26", + "setuptools>=77", + "setuptools-scm>=8", ] [project] @@ -10,62 +10,61 @@ name = "iniconfig" description = "brain-dead simple config-ini parsing" readme = "README.rst" license = "MIT" +license-files = [ "LICENSE" ] authors = [ - { name = "Ronny Pfannschmidt", email = "opensource@ronnypfannschmidt.de" }, - { name = "Holger Krekel", email = "holger.krekel@gmail.com" }, + { name = "Ronny Pfannschmidt", email = "opensource@ronnypfannschmidt.de" }, + { name = "Holger Krekel", email = "holger.krekel@gmail.com" }, +] +requires-python = ">=3.10" +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Operating System :: MacOS :: MacOS X", + "Operating System :: Microsoft :: Windows", + "Operating System :: POSIX", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Topic :: Software Development :: Libraries", + "Topic :: Utilities", ] -requires-python = ">=3.8" dynamic = [ "version", ] -classifiers = [ - "Development Status :: 4 - Beta", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Operating System :: MacOS :: MacOS X", - "Operating System :: Microsoft :: Windows", - "Operating System :: POSIX", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", - "Topic :: Software Development :: Libraries", - "Topic :: Utilities", -] -[project.urls] -Homepage = "https://github.com/pytest-dev/iniconfig" - - -[tool.hatch.version] -source = "vcs" - -[tool.hatch.build.hooks.vcs] -version-file = "src/iniconfig/_version.py" +urls.Homepage = "https://github.com/pytest-dev/iniconfig" -[tool.hatch.build.targets.sdist] -include = [ - "/src", +[dependency-groups] +dev = [ + "pytest>=8.4.2", + "pytest-xdist>=3.8", ] -[tool.hatch.envs.test] -dependencies = [ - "pytest" -] -[tool.hatch.envs.test.scripts] -default = "pytest {args}" +[tool.setuptools] +packages = [ "iniconfig" ] +package-dir = { "" = "src" } -[[tool.hatch.envs.test.matrix]] -python = ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] +[tool.setuptools.package-data] +iniconfig = [ "py.typed" ] [tool.setuptools_scm] +write_to = "src/iniconfig/_version.py" -[tool.mypy] -strict = true +[tool.ruff] +lint.extend-select = [ + "B", # flake8-bugbear + "I", # isort + "PT", # flake8-pytest-style + "UP", # pyupgrade +] +lint.isort.force-single-line = true +lint.isort.known-first-party = [ "iniconfig" ] [tool.pytest.ini_options] testpaths = "testing" + +[tool.mypy] +strict = true diff --git a/src/iniconfig/__init__.py b/src/iniconfig/__init__.py index 3c40bc9..3fadacf 100644 --- a/src/iniconfig/__init__.py +++ b/src/iniconfig/__init__.py @@ -1,42 +1,31 @@ -""" brain-dead simple parser for ini-style files. +"""brain-dead simple parser for ini-style files. (C) Ronny Pfannschmidt, Holger Krekel -- MIT licensed """ -from __future__ import annotations -from typing import ( - Callable, - Iterator, - Mapping, - Optional, - Tuple, - TypeVar, - Union, - TYPE_CHECKING, - NoReturn, - NamedTuple, - overload, - cast, -) import os - -if TYPE_CHECKING: - from typing import Final +from collections.abc import Callable +from collections.abc import Iterator +from collections.abc import Mapping +from typing import Final +from typing import TypeVar +from typing import overload __all__ = ["IniConfig", "ParseError", "COMMENTCHARS", "iscommentline"] -from .exceptions import ParseError from . import _parse -from ._parse import COMMENTCHARS, iscommentline +from ._parse import COMMENTCHARS +from ._parse import iscommentline +from .exceptions import ParseError _D = TypeVar("_D") _T = TypeVar("_T") class SectionWrapper: - config: Final[IniConfig] + config: Final["IniConfig"] name: Final[str] - def __init__(self, config: IniConfig, name: str) -> None: + def __init__(self, config: "IniConfig", name: str) -> None: self.config = config self.name = name @@ -44,16 +33,14 @@ def lineof(self, name: str) -> int | None: return self.config.lineof(self.name, name) @overload - def get(self, key: str) -> str | None: - ... + def get(self, key: str) -> str | None: ... @overload def get( self, key: str, convert: Callable[[str], _T], - ) -> _T | None: - ... + ) -> _T | None: ... @overload def get( @@ -61,12 +48,10 @@ def get( key: str, default: None, convert: Callable[[str], _T], - ) -> _T | None: - ... + ) -> _T | None: ... @overload - def get(self, key: str, default: _D, convert: None = None) -> str | _D: - ... + def get(self, key: str, default: _D, convert: None = None) -> str | _D: ... @overload def get( @@ -74,8 +59,7 @@ def get( key: str, default: _D, convert: Callable[[str], _T], - ) -> _T | _D: - ... + ) -> _T | _D: ... # TODO: investigate possible mypy bug wrt matching the passed over data def get( # type: ignore [misc] @@ -105,6 +89,7 @@ def items(self) -> Iterator[tuple[str, str]]: class IniConfig: path: Final[str] sections: Final[Mapping[str, Mapping[str, str]]] + _sources: Final[Mapping[tuple[str, str | None], int]] def __init__( self, @@ -148,8 +133,7 @@ def get( self, section: str, name: str, - ) -> str | None: - ... + ) -> str | None: ... @overload def get( @@ -157,8 +141,7 @@ def get( section: str, name: str, convert: Callable[[str], _T], - ) -> _T | None: - ... + ) -> _T | None: ... @overload def get( @@ -167,14 +150,12 @@ def get( name: str, default: None, convert: Callable[[str], _T], - ) -> _T | None: - ... + ) -> _T | None: ... @overload def get( self, section: str, name: str, default: _D, convert: None = None - ) -> str | _D: - ... + ) -> str | _D: ... @overload def get( @@ -183,8 +164,7 @@ def get( name: str, default: _D, convert: Callable[[str], _T], - ) -> _T | _D: - ... + ) -> _T | _D: ... def get( # type: ignore self, diff --git a/src/iniconfig/_parse.py b/src/iniconfig/_parse.py index 2d03437..a162636 100644 --- a/src/iniconfig/_parse.py +++ b/src/iniconfig/_parse.py @@ -1,33 +1,31 @@ -from __future__ import annotations -from .exceptions import ParseError - from typing import NamedTuple +from .exceptions import ParseError COMMENTCHARS = "#;" -class _ParsedLine(NamedTuple): +class ParsedLine(NamedTuple): lineno: int section: str | None name: str | None value: str | None -def parse_lines(path: str, line_iter: list[str]) -> list[_ParsedLine]: - result: list[_ParsedLine] = [] +def parse_lines(path: str, line_iter: list[str]) -> list[ParsedLine]: + result: list[ParsedLine] = [] section = None for lineno, line in enumerate(line_iter): name, data = _parseline(path, line, lineno) # new value if name is not None and data is not None: - result.append(_ParsedLine(lineno, section, name, data)) + result.append(ParsedLine(lineno, section, name, data)) # new section elif name is not None and data is None: if not name: raise ParseError(path, lineno, "empty section name") section = name - result.append(_ParsedLine(lineno, section, None, None)) + result.append(ParsedLine(lineno, section, None, None)) # continuation elif name is None and data is not None: if not result: @@ -70,7 +68,7 @@ def _parseline(path: str, line: str, lineno: int) -> tuple[str | None, str | Non try: name, value = line.split(":", 1) except ValueError: - raise ParseError(path, lineno, "unexpected line: %r" % line) + raise ParseError(path, lineno, f"unexpected line: {line!r}") from None return name.strip(), value.strip() # continuation else: diff --git a/src/iniconfig/exceptions.py b/src/iniconfig/exceptions.py index 8c4dc9a..d078bc6 100644 --- a/src/iniconfig/exceptions.py +++ b/src/iniconfig/exceptions.py @@ -1,8 +1,4 @@ -from __future__ import annotations -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Final +from typing import Final class ParseError(Exception): diff --git a/testing/test_iniconfig.py b/testing/test_iniconfig.py index 9e94b28..dd11c73 100644 --- a/testing/test_iniconfig.py +++ b/testing/test_iniconfig.py @@ -1,11 +1,13 @@ -from __future__ import annotations -import pytest -from iniconfig import IniConfig, ParseError, __all__ as ALL -from iniconfig._parse import _ParsedLine as PL -from iniconfig import iscommentline -from textwrap import dedent from pathlib import Path +from textwrap import dedent +import pytest + +from iniconfig import IniConfig +from iniconfig import ParseError +from iniconfig import __all__ as ALL +from iniconfig import iscommentline +from iniconfig._parse import ParsedLine as PL check_tokens: dict[str, tuple[str, list[PL]]] = { "section": ("[section]", [PL(0, "section", None, None)]), @@ -44,7 +46,6 @@ @pytest.fixture(params=sorted(check_tokens)) def input_expected(request: pytest.FixtureRequest) -> tuple[str, list[PL]]: - return check_tokens[request.param] @@ -214,12 +215,12 @@ def test_config_iter() -> None: """ ), ) - l = list(config) - assert len(l) == 2 - assert l[0].name == "section1" - assert l[0]["value"] == "1" - assert l[1].name == "section2" - assert l[1]["value"] == "2" + sections = list(config) + assert len(sections) == 2 + assert sections[0].name == "section1" + assert sections[0]["value"] == "1" + assert sections[1].name == "section2" + assert sections[1]["value"] == "2" def test_config_contains() -> None: @@ -251,8 +252,8 @@ def test_iter_file_order() -> None: b = 2 """, ) - l = list(config) - secnames = [x.name for x in l] + sections_list = list(config) + secnames = [x.name for x in sections_list] assert secnames == ["section2", "section"] assert list(config["section2"]) == ["value", "value2"] assert list(config["section"]) == ["a", "b"] diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..18ebf4f --- /dev/null +++ b/uv.lock @@ -0,0 +1,167 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, +] + +[[package]] +name = "execnet" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/ff/b4c0dc78fbe20c3e59c0c7334de0c27eb4001a2b2017999af398bf730817/execnet-2.1.1.tar.gz", hash = "sha256:5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3", size = 166524, upload-time = "2024-04-08T09:04:19.245Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/09/2aea36ff60d16dd8879bdb2f5b3ee0ba8d08cbbdcdfe870e695ce3784385/execnet-2.1.1-py3-none-any.whl", hash = "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc", size = 40612, upload-time = "2024-04-08T09:04:17.414Z" }, +] + +[[package]] +name = "iniconfig" +source = { editable = "." } + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, + { name = "pytest-xdist" }, +] + +[package.metadata] + +[package.metadata.requires-dev] +dev = [ + { name = "pytest", specifier = ">=8.4.2" }, + { name = "pytest-xdist", specifier = ">=3.8.0" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + +[[package]] +name = "pytest-xdist" +version = "3.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "execnet" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, +] + +[[package]] +name = "tomli" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392, upload-time = "2025-10-08T22:01:47.119Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236, upload-time = "2025-10-08T22:01:00.137Z" }, + { url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084, upload-time = "2025-10-08T22:01:01.63Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832, upload-time = "2025-10-08T22:01:02.543Z" }, + { url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052, upload-time = "2025-10-08T22:01:03.836Z" }, + { url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555, upload-time = "2025-10-08T22:01:04.834Z" }, + { url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128, upload-time = "2025-10-08T22:01:05.84Z" }, + { url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445, upload-time = "2025-10-08T22:01:06.896Z" }, + { url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165, upload-time = "2025-10-08T22:01:08.107Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891, upload-time = "2025-10-08T22:01:09.082Z" }, + { url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796, upload-time = "2025-10-08T22:01:10.266Z" }, + { url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121, upload-time = "2025-10-08T22:01:11.332Z" }, + { url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070, upload-time = "2025-10-08T22:01:12.498Z" }, + { url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859, upload-time = "2025-10-08T22:01:13.551Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296, upload-time = "2025-10-08T22:01:14.614Z" }, + { url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124, upload-time = "2025-10-08T22:01:15.629Z" }, + { url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698, upload-time = "2025-10-08T22:01:16.51Z" }, + { url = "https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", size = 154819, upload-time = "2025-10-08T22:01:17.964Z" }, + { url = "https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", size = 148766, upload-time = "2025-10-08T22:01:18.959Z" }, + { url = "https://files.pythonhosted.org/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", size = 240771, upload-time = "2025-10-08T22:01:20.106Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", size = 248586, upload-time = "2025-10-08T22:01:21.164Z" }, + { url = "https://files.pythonhosted.org/packages/30/77/fed85e114bde5e81ecf9bc5da0cc69f2914b38f4708c80ae67d0c10180c5/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", size = 244792, upload-time = "2025-10-08T22:01:22.417Z" }, + { url = "https://files.pythonhosted.org/packages/55/92/afed3d497f7c186dc71e6ee6d4fcb0acfa5f7d0a1a2878f8beae379ae0cc/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", size = 248909, upload-time = "2025-10-08T22:01:23.859Z" }, + { url = "https://files.pythonhosted.org/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", size = 96946, upload-time = "2025-10-08T22:01:24.893Z" }, + { url = "https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", size = 107705, upload-time = "2025-10-08T22:01:26.153Z" }, + { url = "https://files.pythonhosted.org/packages/19/94/aeafa14a52e16163008060506fcb6aa1949d13548d13752171a755c65611/tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", size = 154244, upload-time = "2025-10-08T22:01:27.06Z" }, + { url = "https://files.pythonhosted.org/packages/db/e4/1e58409aa78eefa47ccd19779fc6f36787edbe7d4cd330eeeedb33a4515b/tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", size = 148637, upload-time = "2025-10-08T22:01:28.059Z" }, + { url = "https://files.pythonhosted.org/packages/26/b6/d1eccb62f665e44359226811064596dd6a366ea1f985839c566cd61525ae/tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", size = 241925, upload-time = "2025-10-08T22:01:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/70/91/7cdab9a03e6d3d2bb11beae108da5bdc1c34bdeb06e21163482544ddcc90/tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", size = 249045, upload-time = "2025-10-08T22:01:31.98Z" }, + { url = "https://files.pythonhosted.org/packages/15/1b/8c26874ed1f6e4f1fcfeb868db8a794cbe9f227299402db58cfcc858766c/tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", size = 245835, upload-time = "2025-10-08T22:01:32.989Z" }, + { url = "https://files.pythonhosted.org/packages/fd/42/8e3c6a9a4b1a1360c1a2a39f0b972cef2cc9ebd56025168c4137192a9321/tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", size = 253109, upload-time = "2025-10-08T22:01:34.052Z" }, + { url = "https://files.pythonhosted.org/packages/22/0c/b4da635000a71b5f80130937eeac12e686eefb376b8dee113b4a582bba42/tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", size = 97930, upload-time = "2025-10-08T22:01:35.082Z" }, + { url = "https://files.pythonhosted.org/packages/b9/74/cb1abc870a418ae99cd5c9547d6bce30701a954e0e721821df483ef7223c/tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", size = 107964, upload-time = "2025-10-08T22:01:36.057Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/5c46fff6432a712af9f792944f4fcd7067d8823157949f4e40c56b8b3c83/tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", size = 163065, upload-time = "2025-10-08T22:01:37.27Z" }, + { url = "https://files.pythonhosted.org/packages/39/67/f85d9bd23182f45eca8939cd2bc7050e1f90c41f4a2ecbbd5963a1d1c486/tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", size = 159088, upload-time = "2025-10-08T22:01:38.235Z" }, + { url = "https://files.pythonhosted.org/packages/26/5a/4b546a0405b9cc0659b399f12b6adb750757baf04250b148d3c5059fc4eb/tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", size = 268193, upload-time = "2025-10-08T22:01:39.712Z" }, + { url = "https://files.pythonhosted.org/packages/42/4f/2c12a72ae22cf7b59a7fe75b3465b7aba40ea9145d026ba41cb382075b0e/tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", size = 275488, upload-time = "2025-10-08T22:01:40.773Z" }, + { url = "https://files.pythonhosted.org/packages/92/04/a038d65dbe160c3aa5a624e93ad98111090f6804027d474ba9c37c8ae186/tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", size = 272669, upload-time = "2025-10-08T22:01:41.824Z" }, + { url = "https://files.pythonhosted.org/packages/be/2f/8b7c60a9d1612a7cbc39ffcca4f21a73bf368a80fc25bccf8253e2563267/tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", size = 279709, upload-time = "2025-10-08T22:01:43.177Z" }, + { url = "https://files.pythonhosted.org/packages/7e/46/cc36c679f09f27ded940281c38607716c86cf8ba4a518d524e349c8b4874/tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", size = 107563, upload-time = "2025-10-08T22:01:44.233Z" }, + { url = "https://files.pythonhosted.org/packages/84/ff/426ca8683cf7b753614480484f6437f568fd2fda2edbdf57a2d3d8b27a0b/tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", size = 119756, upload-time = "2025-10-08T22:01:45.234Z" }, + { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +]