diff --git a/.github/contributing.md b/.github/contributing.md
index b9005a6..b0648e5 100644
--- a/.github/contributing.md
+++ b/.github/contributing.md
@@ -4,18 +4,13 @@ File an issue in GitHub
and we can discuss your bug/feature/idea.
If you are ready to start coding...
-pytest-tap uses the `venv` module
-and a requirements file
-to manage development.
+pytest-tap uses [uv](https://docs.astral.sh/uv/) for development.
```bash
$ git clone git@github.com:python-tap/pytest-tap.git
$ cd pytest-tap
-$ python3 -m venv venv
-$ source venv/bin/activate
-$ pip install -r requirements-dev.txt
$ # Edit some files and run the tests.
-$ pytest
+$ make test
```
Once your feature or bug fix is ready,
@@ -23,17 +18,15 @@ Once your feature or bug fix is ready,
...profit! :moneybag:
-# Making Releases
+# Release checklist
-pytest-tap uses `bump2version`
-to set the proper version
-for releases.
+These are notes for my release process,
+so I don't have to remember all the steps.
+Other contributors are free to ignore this.
-The steps to release are roughly:
-
-```bash
-# Set docs/releases.rst with an appropriate changelog.
-$ bump2version minor
-$ python -m build
-$ twine upload dist/*
-```
+1. Update `docs/releases.rst`.
+2. Update version in `pyproject.toml` and `src/pytest_tap/__init__.py`.
+3. `rm -rf dist && uv build`
+4. `uv publish`
+5. `git tag -a vX.X -m "Version X.X"`
+6. `git push --tags`
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 46c6d1c..0c3ab69 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -3,36 +3,18 @@ name: Python package
on: [push]
jobs:
- lint:
- runs-on: ubuntu-latest
-
- steps:
- - uses: actions/checkout@v3
-
- - name: Setup Python
- uses: actions/setup-python@v4
- with:
- python-version: '3.11'
-
- - name: Install tox and any other packages
- run: pip install tox
-
- - name: Check for lint
- run: tox -e lint
-
build:
- needs: lint
runs-on: ${{ matrix.os }}
strategy:
matrix:
- python: [3.8, 3.9, '3.10', '3.11']
+ python: ['3.9', '3.10', '3.11', '3.12', '3.13']
os: [macos-latest, ubuntu-latest, windows-latest]
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- name: Setup Python
- uses: actions/setup-python@v4
+ uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python }}
@@ -48,12 +30,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- name: Setup Python
- uses: actions/setup-python@v4
+ uses: actions/setup-python@v5
with:
- python-version: '3.11'
+ python-version: '3.13'
- name: Install tox and any other packages
run: pip install tox
@@ -61,8 +43,21 @@ jobs:
- name: Collect coverage data
run: tox -e coverage
- - name: Upload coverage to Codecov
- uses: codecov/codecov-action@v2
+ - name: Combine coverage & fail if it's <100%
+ run: |
+ python -Im pip install --upgrade coverage[toml]
+
+ python -Im coverage html --skip-covered --skip-empty
+
+ # Report and write to summary.
+ python -Im coverage report --format=markdown >> $GITHUB_STEP_SUMMARY
+
+ # Report again and fail if under 100%.
+ python -Im coverage report --fail-under=100
+
+ - name: Upload HTML report if check failed
+ uses: actions/upload-artifact@v4
with:
- fail_ci_if_error: true
- verbose: true
+ name: html-report
+ path: htmlcov
+ if: ${{ failure() }}
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 7b3a438..64192b1 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,11 +1,9 @@
repos:
- - repo: https://github.com/psf/black
- rev: 24.10.0
- hooks:
- - id: black
- language_version: python3.9
- - repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v1.4.0
- hooks:
- - id: flake8
- language_version: python3.9
+- repo: https://github.com/astral-sh/ruff-pre-commit
+ rev: v0.7.4
+ hooks:
+ # Run the linter.
+ - id: ruff
+ args: [ --fix, --unsafe-fixes ]
+ # Run the formatter.
+ - id: ruff-format
diff --git a/.python-version b/.python-version
new file mode 100644
index 0000000..e4fba21
--- /dev/null
+++ b/.python-version
@@ -0,0 +1 @@
+3.12
diff --git a/MANIFEST.in b/MANIFEST.in
deleted file mode 100644
index 4a210a1..0000000
--- a/MANIFEST.in
+++ /dev/null
@@ -1,4 +0,0 @@
-include AUTHORS
-include LICENSE
-include README.rst
-include docs/releases.rst
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..6887d04
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,2 @@
+test:
+ uv run tox -e py312
diff --git a/README.rst b/README.rst
index 31694fc..71789cb 100644
--- a/README.rst
+++ b/README.rst
@@ -9,9 +9,6 @@ pytest-tap
.. |license| image:: https://img.shields.io/pypi/l/pytest-tap.svg
:target: https://raw.githubusercontent.com/python-tap/pytest-tap/master/LICENSE
:alt: BSD license
-.. |coverage| image:: https://img.shields.io/codecov/c/github/python-tap/pytest-tap.svg
- :target: https://codecov.io/github/python-tap/pytest-tap
- :alt: Coverage
Test Anything Protocol (TAP) reporting plugin for
`pytest `_
diff --git a/docs/releases.rst b/docs/releases.rst
index 02cf035..ca1d140 100644
--- a/docs/releases.rst
+++ b/docs/releases.rst
@@ -1,6 +1,28 @@
+pytest-tap is a reporting plugin for pytest that outputs
+`Test Anything Protocol (TAP) `_ data.
+TAP is a line based test protocol for recording test data in a standard way.
+
+Use ``pytest --tap`` after installing to get started.
+
+Follow `GitHub `_
+for more information or to follow this plugin's development.
+Additional developer documentation about Python and TAP is on
+`Read the Docs `_.
+
Releases
========
+Version 3.5, To Be Released
+---------------------------
+
+* Diagnostics now output logs, stdout, stderr for failed tests.
+ Use the standard ``--show-capture`` flag to control the output.
+* Diagnostics can display for passing tests using the
+ ``--tap-log-passing-tests`` flag.
+* Add support for Python 3.12.
+* Add support for Python 3.13.
+* Drop support for Python 3.8 (it is end-of-life).
+
Version 3.4, July 15, 2023
--------------------------
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..940540c
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,83 @@
+[project]
+name = "pytest-tap"
+version = "3.5"
+description = "Test Anything Protocol (TAP) reporting plugin for pytest"
+readme = "docs/releases.rst"
+authors = [
+ { name = "Matt Layman", email = "matthewlayman@gmail.com" }
+]
+license = { text = "BSD" }
+homepage = "https://github.com/python-tap/pytest-tap"
+dependencies = [
+ "pytest>=3.0",
+ "tap.py>=3.2,<4.0"
+]
+requires-python = ">=3.9"
+keywords = ["TAP", "unittest", "pytest"]
+classifiers = [
+ "Development Status :: 5 - Production/Stable",
+ "Framework :: Pytest",
+ "Intended Audience :: Developers",
+ "License :: OSI Approved :: BSD License",
+ "Operating System :: OS Independent",
+ "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 :: Testing"
+]
+
+[project.entry-points.pytest11]
+tap = "pytest_tap.plugin"
+
+[dependency-groups]
+dev = [
+ "pytest-tap",
+ "tox>=4.24.1",
+]
+
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[tool.hatch.build.targets.sdist]
+include = [
+ "/src",
+ "/docs",
+ "/tests",
+]
+sources = ["src"]
+
+[tool.hatch.build.targets.wheel]
+sources = ["src"]
+packages = ["pytest_tap"]
+
+[tool.pytest.ini_options]
+pythonpath = [".", "src"]
+
+[tool.uv.sources]
+pytest-tap = { workspace = true }
+
+[tool.ruff.lint]
+select = [
+ # pycodestyle
+ "E",
+ "W",
+ # Pyflakes
+ "F",
+ # pyupgrade
+ "UP",
+ # flake8-bandit
+ "S",
+ # flake8-bugbear
+ "B",
+ # flake8-simplify
+ "SIM",
+ # isort
+ "I",
+]
+ignore = [
+ # bandit: Use of `assert` detected
+ "S101",
+]
diff --git a/requirements-dev.txt b/requirements-dev.txt
deleted file mode 100644
index d615399..0000000
--- a/requirements-dev.txt
+++ /dev/null
@@ -1,10 +0,0 @@
-black
-build
-bump2version
-flake8
-pre-commit
-requests
-tox
-twine
-wheel
--e .
diff --git a/setup.cfg b/setup.cfg
deleted file mode 100644
index ab29160..0000000
--- a/setup.cfg
+++ /dev/null
@@ -1,16 +0,0 @@
-[bumpversion]
-current_version = 3.4
-commit = True
-tag = True
-parse = (?P\d+)\.(?P\d+)
-serialize = {major}.{minor}
-
-[bumpversion:file:setup.py]
-
-[bumpversion:file:src/pytest_tap/__init__.py]
-
-[metadata]
-license_files = LICENSE
-
-[flake8]
-max-line-length = 88
diff --git a/setup.py b/setup.py
deleted file mode 100644
index fb9cb4a..0000000
--- a/setup.py
+++ /dev/null
@@ -1,53 +0,0 @@
-"""
-pytest-tap is a reporting plugin for pytest that outputs
-`Test Anything Protocol (TAP) `_ data.
-TAP is a line based test protocol for recording test data in a standard way.
-
-Use ``pytest --tap`` after installing to get started.
-
-Follow `GitHub `_
-for more information or to follow this plugin's development.
-Additional developer documentation about Python and TAP is on
-`Read the Docs `_.
-"""
-
-from setuptools import find_packages, setup
-
-
-if __name__ == "__main__":
- with open("docs/releases.rst", "r") as f:
- releases = f.read()
-
- long_description = __doc__ + "\n\n" + releases
-
- setup(
- name="pytest-tap",
- version="3.4",
- url="https://github.com/python-tap/pytest-tap",
- license="BSD",
- author="Matt Layman",
- author_email="matthewlayman@gmail.com",
- description="Test Anything Protocol (TAP) reporting plugin for pytest",
- long_description=long_description,
- packages=find_packages(where="src"),
- package_dir={"": "src"},
- entry_points={"pytest11": ["tap = pytest_tap.plugin"]},
- include_package_data=True,
- zip_safe=False,
- platforms="any",
- install_requires=["pytest>=3.0", "tap.py>=3.2,<4.0"],
- classifiers=[
- "Development Status :: 5 - Production/Stable",
- "Framework :: Pytest",
- "Intended Audience :: Developers",
- "License :: OSI Approved :: BSD License",
- "Operating System :: OS Independent",
- "Programming Language :: Python :: 3.7",
- "Programming Language :: Python :: 3.8",
- "Programming Language :: Python :: 3.9",
- "Programming Language :: Python :: 3.10",
- "Programming Language :: Python :: Implementation :: PyPy",
- "Topic :: Software Development :: Testing",
- ],
- keywords=["TAP", "unittest", "pytest"],
- )
diff --git a/src/pytest_tap/__init__.py b/src/pytest_tap/__init__.py
index 31b8f46..8dfe710 100644
--- a/src/pytest_tap/__init__.py
+++ b/src/pytest_tap/__init__.py
@@ -1 +1 @@
-__version__ = "3.4"
+__version__ = "3.5"
diff --git a/src/pytest_tap/plugin.py b/src/pytest_tap/plugin.py
index 08a8121..a952bd4 100644
--- a/src/pytest_tap/plugin.py
+++ b/src/pytest_tap/plugin.py
@@ -29,8 +29,8 @@ def __init__(self, config: pytest.Config) -> None:
# Disable it automatically for streaming.
self._tracker.header = False
- self.tap_logging = config.option.showcapture
- self.tap_log_passing_tests = config.option.tap_log_passing_tests
+ self.show_capture = config.option.showcapture
+ self.log_passing_tests = config.option.tap_log_passing_tests
@pytest.hookimpl()
def pytest_runtestloop(self, session):
@@ -68,21 +68,21 @@ def pytest_runtest_logreport(self, report: pytest.TestReport):
# for that decorator.
# Ignore the "reason: " from pytest.
if report.wasxfail and report.wasxfail != "reason: ":
- reason = ": {}".format(report.wasxfail)
+ reason = f": {report.wasxfail}"
if report.skipped:
- directive = "TODO expected failure{}".format(reason)
+ directive = f"TODO expected failure{reason}"
self._tracker.add_not_ok(testcase, description, directive=directive)
elif report.passed:
- directive = "TODO unexpected success{}".format(reason)
+ directive = f"TODO unexpected success{reason}"
self._tracker.add_ok(testcase, description, directive=directive)
elif report.passed:
diagnostics = None
- if self.tap_log_passing_tests:
- diagnostics = _make_as_diagnostics(report, self.tap_logging)
+ if self.log_passing_tests:
+ diagnostics = _make_as_diagnostics(report, self.show_capture)
self._tracker.add_ok(testcase, description, diagnostics=diagnostics)
elif report.failed:
- diagnostics = _make_as_diagnostics(report, self.tap_logging)
+ diagnostics = _make_as_diagnostics(report, self.show_capture)
# pytest treats an unexpected success from unitest.expectedFailure
# as a failure.
@@ -104,7 +104,7 @@ def pytest_runtest_logreport(self, report: pytest.TestReport):
self._tracker.add_not_ok(
testcase,
description,
- directive="unexpected success: {}".format(report.longrepr),
+ directive=f"unexpected success: {report.longrepr}",
)
return
@@ -177,23 +177,23 @@ def pytest_configure(config: pytest.Config) -> None:
config.pluginmanager.register(TAPPlugin(config), "tapplugin")
-def _make_as_diagnostics(report, tap_logging):
+def _make_as_diagnostics(report, show_capture):
"""Format a report as TAP diagnostic output."""
lines = report.longreprtext.splitlines(keepends=True)
- if tap_logging in SHOW_CAPTURE_LOG:
+ if show_capture in SHOW_CAPTURE_LOG:
if lines:
lines[-1] += "\n"
lines += ["--- Captured Log ---\n"] + (
report.caplog.splitlines(keepends=True) or [""]
)
- if tap_logging in SHOW_CAPTURE_OUT:
+ if show_capture in SHOW_CAPTURE_OUT:
if lines:
lines[-1] += "\n"
lines += ["--- Captured Out ---\n"] + (
report.capstdout.splitlines(keepends=True) or [""]
)
- if tap_logging in SHOW_CAPTUER_ERR:
+ if show_capture in SHOW_CAPTUER_ERR:
if lines:
lines[-1] += "\n"
lines += ["--- Captured Err ---\n"] + (
diff --git a/tox.ini b/tox.ini
index ef7e60c..3c9f296 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,7 +1,5 @@
[tox]
envlist =
- py37
- lint
coverage
[testenv]
@@ -9,14 +7,6 @@ deps =
pytest
commands = pytest --tap-combined {posargs}
-[testenv:lint]
-deps =
- black
- flake8
-commands =
- black --check src setup.py
- flake8 src setup.py
-
[testenv:coverage]
deps =
pytest
diff --git a/uv.lock b/uv.lock
new file mode 100644
index 0000000..f0a4b26
--- /dev/null
+++ b/uv.lock
@@ -0,0 +1,242 @@
+version = 1
+requires-python = ">=3.9"
+
+[[package]]
+name = "cachetools"
+version = "5.5.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d9/74/57df1ab0ce6bc5f6fa868e08de20df8ac58f9c44330c7671ad922d2bbeae/cachetools-5.5.1.tar.gz", hash = "sha256:70f238fbba50383ef62e55c6aff6d9673175fe59f7c6782c7a0b9e38f4a9df95", size = 28044 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ec/4e/de4ff18bcf55857ba18d3a4bd48c8a9fde6bb0980c9d20b263f05387fd88/cachetools-5.5.1-py3-none-any.whl", hash = "sha256:b76651fdc3b24ead3c648bbdeeb940c1b04d365b38b4af66788f9ec4a81d42bb", size = 9530 },
+]
+
+[[package]]
+name = "chardet"
+version = "5.2.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/f7b6ab21ec75897ed80c17d79b15951a719226b9fababf1e40ea74d69079/chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7", size = 2069618 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/38/6f/f5fbc992a329ee4e0f288c1fe0e2ad9485ed064cac731ed2fe47dcc38cbf/chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970", size = 199385 },
+]
+
+[[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 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
+]
+
+[[package]]
+name = "distlib"
+version = "0.3.9"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/0d/dd/1bec4c5ddb504ca60fc29472f3d27e8d4da1257a854e1d96742f15c1d02d/distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403", size = 613923 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973 },
+]
+
+[[package]]
+name = "exceptiongroup"
+version = "1.2.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 },
+]
+
+[[package]]
+name = "filelock"
+version = "3.17.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/dc/9c/0b15fb47b464e1b663b1acd1253a062aa5feecb07d4e597daea542ebd2b5/filelock-3.17.0.tar.gz", hash = "sha256:ee4e77401ef576ebb38cd7f13b9b28893194acc20a8e68e18730ba9c0e54660e", size = 18027 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/89/ec/00d68c4ddfedfe64159999e5f8a98fb8442729a63e2077eb9dcd89623d27/filelock-3.17.0-py3-none-any.whl", hash = "sha256:533dc2f7ba78dc2f0f531fc6c4940addf7b70a481e269a5a3b93be94ffbe8338", size = 16164 },
+]
+
+[[package]]
+name = "iniconfig"
+version = "2.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 },
+]
+
+[[package]]
+name = "packaging"
+version = "24.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 },
+]
+
+[[package]]
+name = "platformdirs"
+version = "4.3.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 },
+]
+
+[[package]]
+name = "pluggy"
+version = "1.5.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 },
+]
+
+[[package]]
+name = "pyproject-api"
+version = "1.9.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "packaging" },
+ { name = "tomli", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/7e/66/fdc17e94486836eda4ba7113c0db9ac7e2f4eea1b968ee09de2fe75e391b/pyproject_api-1.9.0.tar.gz", hash = "sha256:7e8a9854b2dfb49454fae421cb86af43efbb2b2454e5646ffb7623540321ae6e", size = 22714 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b0/1d/92b7c765df46f454889d9610292b0ccab15362be3119b9a624458455e8d5/pyproject_api-1.9.0-py3-none-any.whl", hash = "sha256:326df9d68dea22d9d98b5243c46e3ca3161b07a1b9b18e213d1e24fd0e605766", size = 13131 },
+]
+
+[[package]]
+name = "pytest"
+version = "8.3.4"
+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 = "tomli", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/05/35/30e0d83068951d90a01852cb1cef56e5d8a09d20c7f511634cc2f7e0372a/pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761", size = 1445919 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/11/92/76a1c94d3afee238333bc0a42b82935dd8f9cf8ce9e336ff87ee14d9e1cf/pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", size = 343083 },
+]
+
+[[package]]
+name = "pytest-tap"
+version = "3.4"
+source = { editable = "." }
+dependencies = [
+ { name = "pytest" },
+ { name = "tap-py" },
+]
+
+[package.dev-dependencies]
+dev = [
+ { name = "pytest-tap" },
+ { name = "tox" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "pytest", specifier = ">=3.0" },
+ { name = "tap-py", specifier = ">=3.2,<4.0" },
+]
+
+[package.metadata.requires-dev]
+dev = [
+ { name = "pytest-tap", editable = "." },
+ { name = "tox", specifier = ">=4.24.1" },
+]
+
+[[package]]
+name = "tap-py"
+version = "3.2.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/37/d0/28b0c6c7fe464f70664f123e9f64f36acaf92767af98c1a82df6e2b49f41/tap_py-3.2.1.tar.gz", hash = "sha256:d03c9e6af0a56fad994f1c69f14041e676811d73eeeef20bf4077c43d621d608", size = 125390 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6c/9e/6445a273190f69ae06989e1675f081206dd6b20dff19e570304b94e8350a/tap_py-3.2.1-py3-none-any.whl", hash = "sha256:ae2a8a7999529ec568d03462e75968289cee5927dfd52fcc3a6381c8892d10ef", size = 19264 },
+]
+
+[[package]]
+name = "tomli"
+version = "2.2.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 },
+ { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 },
+ { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 },
+ { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 },
+ { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 },
+ { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 },
+ { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 },
+ { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 },
+ { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 },
+ { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 },
+ { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762 },
+ { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453 },
+ { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486 },
+ { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349 },
+ { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159 },
+ { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243 },
+ { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645 },
+ { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584 },
+ { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 },
+ { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 },
+ { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708 },
+ { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582 },
+ { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543 },
+ { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691 },
+ { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170 },
+ { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530 },
+ { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666 },
+ { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954 },
+ { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 },
+ { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 },
+ { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 },
+]
+
+[[package]]
+name = "tox"
+version = "4.24.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cachetools" },
+ { name = "chardet" },
+ { name = "colorama" },
+ { name = "filelock" },
+ { name = "packaging" },
+ { name = "platformdirs" },
+ { name = "pluggy" },
+ { name = "pyproject-api" },
+ { name = "tomli", marker = "python_full_version < '3.11'" },
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
+ { name = "virtualenv" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/cf/7b/97f757e159983737bdd8fb513f4c263cd411a846684814ed5433434a1fa9/tox-4.24.1.tar.gz", hash = "sha256:083a720adbc6166fff0b7d1df9d154f9d00bfccb9403b8abf6bc0ee435d6a62e", size = 194742 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ab/04/b0d1c1b44c98583cab9eabb4acdba964fdf6b6c597c53cfb8870fd08cbbf/tox-4.24.1-py3-none-any.whl", hash = "sha256:57ba7df7d199002c6df8c2db9e6484f3de6ca8f42013c083ea2d4d1e5c6bdc75", size = 171829 },
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.12.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 },
+]
+
+[[package]]
+name = "virtualenv"
+version = "20.29.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "distlib" },
+ { name = "filelock" },
+ { name = "platformdirs" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a7/ca/f23dcb02e161a9bba141b1c08aa50e8da6ea25e6d780528f1d385a3efe25/virtualenv-20.29.1.tar.gz", hash = "sha256:b8b8970138d32fb606192cb97f6cd4bb644fa486be9308fb9b63f81091b5dc35", size = 7658028 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/89/9b/599bcfc7064fbe5740919e78c5df18e5dceb0887e676256a1061bb5ae232/virtualenv-20.29.1-py3-none-any.whl", hash = "sha256:4e4cb403c0b0da39e13b46b1b2476e505cb0046b25f242bee80f62bf990b2779", size = 4282379 },
+]