diff --git a/.bumpversion.cfg b/.bumpversion.cfg
index 83e32e00..20f27b99 100644
--- a/.bumpversion.cfg
+++ b/.bumpversion.cfg
@@ -3,9 +3,9 @@ current_version = 6.3.0
commit = True
tag = True
-[bumpversion:file:setup.py]
-search = version='{current_version}'
-replace = version='{new_version}'
+[bumpversion:file:pyproject.toml]
+search = version = "{current_version}"
+replace = version = "{new_version}"
[bumpversion:file (badge):README.rst]
search = /v{current_version}.svg
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 1d2f108a..e8687d3d 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -6,15 +6,21 @@ exclude: '^(\.tox|ci/templates|\.bumpversion\.cfg)(/|$)'
# Note the order is intentional to avoid multiple passes of the hooks
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: v0.11.13
+ rev: v0.12.12
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix, --show-fixes, --unsafe-fixes]
- id: ruff-format
+ - repo: https://github.com/ComPWA/taplo-pre-commit
+ rev: v0.9.3
+ hooks:
+ - id: taplo-format
+ - id: taplo-lint
- repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v5.0.0
+ rev: v6.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- exclude: '.*\.pth$'
+ - id: mixed-line-ending
+ args: [--fix=lf]
- id: debug-statements
diff --git a/.taplo.toml b/.taplo.toml
new file mode 100644
index 00000000..8f8054d3
--- /dev/null
+++ b/.taplo.toml
@@ -0,0 +1,3 @@
+[formatting]
+array_auto_collapse = false
+indent_string = " "
diff --git a/AUTHORS.rst b/AUTHORS.rst
index bcb66ddc..42933ffa 100644
--- a/AUTHORS.rst
+++ b/AUTHORS.rst
@@ -64,3 +64,4 @@ Authors
* Dawn James - https://github.com/dawngerpony
* Tsvika Shapira - https://github.com/tsvikas
* Marcos Boger - https://github.com/marcosboger
+* Ofek Lev - https://github.com/ofek
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 3ac7b56a..4524aad3 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -20,6 +20,11 @@ Changelog
patch = subprocess
This release also requires at least coverage 7.10.6.
+* Switched packaging to have metadata completely in ``pyproject.toml`` and use `hatchling `_ for
+ building.
+ Contributed by Ofek Lev in `#551 `_
+ with some extras in `#716 `_.
+* Removed some not really necessary testing deps like ``six``.
6.3.0 (2025-09-06)
------------------
@@ -277,8 +282,8 @@ Changelog
------------------
* Fixed ``RecursionError`` that can occur when using
- `cleanup_on_signal `__ or
- `cleanup_on_sigterm `__.
+ `cleanup_on_signal `__ or
+ `cleanup_on_sigterm `__.
See: `#294 `_.
The 2.7.x releases of pytest-cov should be considered broken regarding aforementioned cleanup API.
* Added compatibility with future xdist release that deprecates some internals
diff --git a/MANIFEST.in b/MANIFEST.in
deleted file mode 100644
index 529ba8f4..00000000
--- a/MANIFEST.in
+++ /dev/null
@@ -1,30 +0,0 @@
-graft docs
-graft examples
-prune examples/*/.tox
-prune examples/*/htmlcov
-prune examples/*/*/htmlcov
-prune examples/adhoc-layout/*.egg-info
-prune examples/src-layout/src/*.egg-info
-
-graft .github/workflows
-graft src
-graft ci
-graft tests
-
-include .bumpversion.cfg
-include .cookiecutterrc
-include .coveragerc
-include .editorconfig
-include .pre-commit-config.yaml
-include .readthedocs.yml
-include pytest.ini
-include tox.ini
-
-include AUTHORS.rst
-include CHANGELOG.rst
-include CONTRIBUTING.rst
-include LICENSE
-include README.rst
-include SECURITY.md
-
-global-exclude *.py[cod] __pycache__/* *.so *.dylib
diff --git a/ci/bootstrap.py b/ci/bootstrap.py
index 08d6c90b..ee69e2c5 100755
--- a/ci/bootstrap.py
+++ b/ci/bootstrap.py
@@ -20,8 +20,6 @@ def exec_in_env():
else:
bin_path = env_path / 'bin'
if not env_path.exists():
- import subprocess
-
print(f'Making bootstrap env in: {env_path} ...')
try:
check_call([sys.executable, '-m', 'venv', env_path])
@@ -59,7 +57,7 @@ def main():
# This uses sys.executable the same way that the call in
# cookiecutter-pylibrary/hooks/post_gen_project.py
# invokes this bootstrap.py itself.
- for line in subprocess.check_output([sys.executable, '-m', 'tox', '--listenvs'], text=True).splitlines()
+ for line in subprocess.check_output([sys.executable, '-m', 'tox', '--listenvs'], universal_newlines=True).splitlines()
]
tox_environments = [line for line in tox_environments if line.startswith('py')]
for template in templates_path.rglob('*'):
diff --git a/ci/requirements.txt b/ci/requirements.txt
index b4f18520..fdb6b93a 100644
--- a/ci/requirements.txt
+++ b/ci/requirements.txt
@@ -1,5 +1,4 @@
-virtualenv>=16.6.0
-pip>=19.1.1
-setuptools>=18.0.1
-tox
-twine
+pip>=25
+setuptools>=80
+tox>=4
+virtualenv>=20.34
diff --git a/docs/releasing.rst b/docs/releasing.rst
index 9afe600d..3032344d 100644
--- a/docs/releasing.rst
+++ b/docs/releasing.rst
@@ -23,7 +23,7 @@ The process for releasing should follow these steps:
These files need to be removed to force distutils/setuptools to rebuild everything and recreate the egg-info metadata.
#. Build the dists::
- python3 setup.py clean --all sdist bdist_wheel
+ python -m build
#. Verify that the resulting archives (found in ``dist/``) are good.
#. Upload the sdist and wheel with twine::
diff --git a/pyproject.toml b/pyproject.toml
index e795c6de..930b3b5f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,76 @@
[build-system]
-requires = [
- "setuptools>=30.3.0",
+requires = ["hatchling", "hatch-fancy-pypi-readme"]
+build-backend = "hatchling.build"
+
+[project]
+name = "pytest-cov"
+dynamic = ["readme"]
+version = "6.3.0"
+description = "Pytest plugin for measuring coverage."
+license = "MIT"
+requires-python = ">=3.9"
+authors = [
+ { name = "Marc Schlaich", email = "marc.schlaich@gmail.com" },
+]
+maintainers = [
+ { name = "Ionel Cristian Mărieș", email = "contact@ionelmc.ro" },
+]
+keywords = [
+ "cover",
+ "coverage",
+ "distributed",
+ "parallel",
+ "py.test",
+ "pytest",
+]
+classifiers = [
+ "Development Status :: 5 - Production/Stable",
+ "Framework :: Pytest",
+ "Intended Audience :: Developers",
+ "License :: OSI Approved :: MIT License",
+ "Operating System :: Microsoft :: Windows",
+ "Operating System :: POSIX",
+ "Operating System :: Unix",
+ "Programming Language :: Python",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3 :: Only",
+ "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",
+ "Programming Language :: Python :: Implementation :: CPython",
+ "Programming Language :: Python :: Implementation :: PyPy",
+ "Topic :: Software Development :: Testing",
+ "Topic :: Utilities",
+]
+dependencies = [
+ "coverage[toml]>=7.10.6",
+ "pytest>=7",
+ "pluggy>=1.2",
+]
+
+[project.optional-dependencies]
+testing = [
+ "process-tests",
+ "pytest-xdist",
+ "virtualenv",
+]
+
+[project.entry-points.pytest11]
+pytest_cov = "pytest_cov.plugin"
+
+[project.urls]
+"Sources" = "https://github.com/pytest-dev/pytest-cov"
+"Documentation" = "https://pytest-cov.readthedocs.io/"
+"Changelog" = "https://pytest-cov.readthedocs.io/en/latest/changelog.html"
+"Issue Tracker" = "https://github.com/pytest-dev/pytest-cov/issues"
+
+[tool.hatch.metadata.hooks.fancy-pypi-readme]
+content-type = "text/x-rst"
+fragments = [
+ { path = "README.rst" },
+ { path = "CHANGELOG.rst" },
]
[tool.ruff]
@@ -14,30 +84,31 @@ target-version = "py39"
[tool.ruff.lint]
ignore = [
- "RUF001", # ruff-specific rules ambiguous-unicode-character-string
- "S101", # flake8-bandit assert
- "S308", # flake8-bandit suspicious-mark-safe-usage
- "E501", # pycodestyle line-too-long
+ "PLC0415", # `import` should be at the top-level of a file
+ "RUF001", # ruff-specific rules ambiguous-unicode-character-string
+ "S101", # flake8-bandit assert
+ "S308", # flake8-bandit suspicious-mark-safe-usage
+ "E501", # pycodestyle line-too-long
]
select = [
- "B", # flake8-bugbear
- "C4", # flake8-comprehensions
+ "B", # flake8-bugbear
+ "C4", # flake8-comprehensions
"DTZ", # flake8-datetimez
- "E", # pycodestyle errors
+ "E", # pycodestyle errors
"EXE", # flake8-executable
- "F", # pyflakes
- "I", # isort
+ "F", # pyflakes
+ "I", # isort
"INT", # flake8-gettext
"PIE", # flake8-pie
"PLC", # pylint convention
"PLE", # pylint errors
- "PT", # flake8-pytest-style
+ "PT", # flake8-pytest-style
"PTH", # flake8-use-pathlib
"RSE", # flake8-raise
"RUF", # ruff-specific rules
- "S", # flake8-bandit
- "UP", # pyupgrade
- "W", # pycodestyle warnings
+ "S", # flake8-bandit
+ "UP", # pyupgrade
+ "W", # pycodestyle warnings
]
[tool.ruff.lint.flake8-pytest-style]
diff --git a/setup.py b/setup.py
deleted file mode 100755
index 46d25a81..00000000
--- a/setup.py
+++ /dev/null
@@ -1,82 +0,0 @@
-#!/usr/bin/env python
-
-import re
-from pathlib import Path
-
-from setuptools import find_packages
-from setuptools import setup
-
-
-def read(*names, **kwargs):
- with Path(__file__).parent.joinpath(*names).open(encoding=kwargs.get('encoding', 'utf8')) as fh:
- return fh.read()
-
-
-setup(
- name='pytest-cov',
- version='6.3.0',
- license='MIT',
- description='Pytest plugin for measuring coverage.',
- long_description='{}\n{}'.format(read('README.rst'), re.sub(':[a-z]+:`~?(.*?)`', r'``\1``', read('CHANGELOG.rst'))),
- author='Marc Schlaich',
- author_email='marc.schlaich@gmail.com',
- url='https://github.com/pytest-dev/pytest-cov',
- packages=find_packages('src'),
- package_dir={'': 'src'},
- py_modules=[path.stem for path in Path('src').glob('*.py')],
- include_package_data=True,
- zip_safe=False,
- classifiers=[
- # complete classifier list: http://pypi.python.org/pypi?%3Aaction=list_classifiers
- 'Development Status :: 5 - Production/Stable',
- 'Framework :: Pytest',
- 'Intended Audience :: Developers',
- 'Operating System :: Microsoft :: Windows',
- 'Operating System :: POSIX',
- 'Operating System :: Unix',
- 'Programming Language :: Python',
- 'Programming Language :: Python :: 3',
- 'Programming Language :: Python :: 3 :: Only',
- 'Programming Language :: Python :: 3.9',
- 'Programming Language :: Python :: 3.10',
- 'Programming Language :: Python :: 3.11',
- 'Programming Language :: Python :: 3.12',
- 'Programming Language :: Python :: Implementation :: CPython',
- 'Programming Language :: Python :: Implementation :: PyPy',
- 'Topic :: Software Development :: Testing',
- 'Topic :: Utilities',
- ],
- project_urls={
- 'Documentation': 'https://pytest-cov.readthedocs.io/',
- 'Changelog': 'https://pytest-cov.readthedocs.io/en/latest/changelog.html',
- 'Issue Tracker': 'https://github.com/pytest-dev/pytest-cov/issues',
- },
- keywords=[
- 'cover',
- 'coverage',
- 'pytest',
- 'py.test',
- 'distributed',
- 'parallel',
- ],
- python_requires='>=3.9',
- install_requires=[
- 'pytest>=6.2.5',
- 'coverage[toml]>=7.10.6',
- 'pluggy>=1.2',
- ],
- extras_require={
- 'testing': [
- 'fields',
- 'hunter',
- 'process-tests',
- 'pytest-xdist',
- 'virtualenv',
- ]
- },
- entry_points={
- 'pytest11': [
- 'pytest_cov = pytest_cov.plugin',
- ],
- },
-)
diff --git a/tests/test_pytest_cov.py b/tests/test_pytest_cov.py
index a17c4aa3..6ca09fe5 100644
--- a/tests/test_pytest_cov.py
+++ b/tests/test_pytest_cov.py
@@ -1,27 +1,21 @@
-# ruff: noqa
import collections
import glob
import os
import platform
import re
-import subprocess
import sys
-from io import StringIO
from itertools import chain
+from pathlib import Path
+from types import SimpleNamespace
import coverage
-import py
import pytest
-import virtualenv
-import xdist
-from fields import Namespace
from process_tests import TestProcess as _TestProcess
from process_tests import dump_on_error
from process_tests import wait_for_strings
import pytest_cov.plugin
-
max_worker_restart_0 = '--max-worker-restart=0'
SCRIPT = """
@@ -167,7 +161,7 @@ def test_foo(cov):
def adjust_sys_path():
"""Adjust PYTHONPATH during tests to make "helper" importable in SCRIPT."""
orig_path = os.environ.get('PYTHONPATH', None)
- new_path = os.path.dirname(__file__)
+ new_path = str(Path(__file__).parent)
if orig_path is not None:
new_path = os.pathsep.join([new_path, orig_path])
os.environ['PYTHONPATH'] = new_path
@@ -190,7 +184,7 @@ def adjust_sys_path():
ids=['branch2x', 'branch1c', 'branch1a', 'nobranch'],
)
def prop(request):
- return Namespace(
+ return SimpleNamespace(
code=SCRIPT,
code2=SCRIPT2,
conf=request.param[0],
@@ -347,7 +341,7 @@ def test_xml_output_dir(testdir):
def test_json_output_dir(testdir):
script = testdir.makepyfile(SCRIPT)
- result = testdir.runpytest('-v', '--cov=%s' % script.dirpath(), '--cov-report=json:' + JSON_REPORT_NAME, script)
+ result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=json:' + JSON_REPORT_NAME, script)
result.stdout.fnmatch_lines(
[
@@ -363,7 +357,7 @@ def test_json_output_dir(testdir):
def test_markdown_output_dir(testdir):
script = testdir.makepyfile(SCRIPT)
- result = testdir.runpytest('-v', '--cov=%s' % script.dirpath(), '--cov-report=markdown:' + MARKDOWN_REPORT_NAME, script)
+ result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=markdown:' + MARKDOWN_REPORT_NAME, script)
result.stdout.fnmatch_lines(
[
@@ -379,7 +373,7 @@ def test_markdown_output_dir(testdir):
def test_markdown_append_output_dir(testdir):
script = testdir.makepyfile(SCRIPT)
- result = testdir.runpytest('-v', '--cov=%s' % script.dirpath(), '--cov-report=markdown-append:' + MARKDOWN_APPEND_REPORT_NAME, script)
+ result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=markdown-append:' + MARKDOWN_APPEND_REPORT_NAME, script)
result.stdout.fnmatch_lines(
[
@@ -397,7 +391,7 @@ def test_markdown_and_markdown_append_work_together(testdir):
result = testdir.runpytest(
'-v',
- '--cov=%s' % script.dirpath(),
+ f'--cov={script.dirpath()}',
'--cov-report=markdown:' + MARKDOWN_REPORT_NAME,
'--cov-report=markdown-append:' + MARKDOWN_APPEND_REPORT_NAME,
script,
@@ -420,7 +414,7 @@ def test_markdown_and_markdown_append_pointing_to_same_file_throws_error(testdir
result = testdir.runpytest(
'-v',
- '--cov=%s' % script.dirpath(),
+ f'--cov={script.dirpath()}',
'--cov-report=markdown:' + MARKDOWN_REPORT_NAME,
'--cov-report=markdown-append:' + MARKDOWN_REPORT_NAME,
script,
@@ -466,7 +460,7 @@ def test_term_missing_output_dir(testdir):
result.stderr.fnmatch_lines(
[
- '*argument --cov-report: output specifier not supported for: "term-missing:%s"*' % DEST_DIR,
+ f'*argument --cov-report: output specifier not supported for: "term-missing:{DEST_DIR}"*',
]
)
assert result.ret != 0
@@ -755,7 +749,7 @@ def test_add_task(celery_worker):
result.stdout.fnmatch_lines(
[
'*_ coverage: platform *, python * _*',
- f'small_celery* 100%*',
+ 'small_celery* 100%*',
]
)
assert result.ret == 0
@@ -1353,7 +1347,7 @@ def test_run():
'-v', '--assert=plain', f'--cov={script.dirpath()}', '--cov-report=term-missing', '--cov-report=html', script
)
- result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', f'test_cleanup_on_sigterm* 88% * 18-19', '*1 passed*'])
+ result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', 'test_cleanup_on_sigterm* 88% * 18-19', '*1 passed*'])
assert result.ret == 0
@@ -1649,7 +1643,7 @@ def test_dist_bare_cov(testdir):
def test_not_started_plugin_does_not_fail(testdir):
class ns:
- cov_source = [True]
+ cov_source = (True,)
cov_report = ''
plugin = pytest_cov.plugin.CovPlugin(ns, None, start=False)
@@ -1697,14 +1691,13 @@ def test_external_data_file(testdir):
testdir.tmpdir.join('.coveragerc').write(
"""
[run]
-data_file = %s
-"""
- % testdir.tmpdir.join('some/special/place/coverage-data').ensure()
+data_file = {}
+""".format(testdir.tmpdir.join('some/special/place/coverage-data').ensure())
)
result = testdir.runpytest('-v', f'--cov={script.dirpath()}', script)
assert result.ret == 0
- assert glob.glob(str(testdir.tmpdir.join('some/special/place/coverage-data*')))
+ assert glob.glob(str(testdir.tmpdir.join('some/special/place/coverage-data*'))) # noqa: PTH207
@pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"')
@@ -1714,14 +1707,13 @@ def test_external_data_file_xdist(testdir):
"""
[run]
parallel = true
-data_file = %s
-"""
- % testdir.tmpdir.join('some/special/place/coverage-data').ensure()
+data_file = {}
+""".format(testdir.tmpdir.join('some/special/place/coverage-data').ensure())
)
result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '-n', '1', max_worker_restart_0, script)
assert result.ret == 0
- assert glob.glob(str(testdir.tmpdir.join('some/special/place/coverage-data*')))
+ assert glob.glob(str(testdir.tmpdir.join('some/special/place/coverage-data*'))) # noqa: PTH207
@pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"')
@@ -1748,7 +1740,7 @@ def test_external_data_file_negative(testdir):
result = testdir.runpytest('-v', f'--cov={script.dirpath()}', script)
assert result.ret == 0
- assert glob.glob(str(testdir.tmpdir.join('.coverage*')))
+ assert glob.glob(str(testdir.tmpdir.join('.coverage*'))) # noqa: PTH207
@xdist_params
@@ -1856,7 +1848,7 @@ def test_do_not_append_coverage(pytester, testdir, opts, prop):
@pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"')
def test_append_coverage_subprocess(testdir):
- testdir.makepyprojecttoml(f"""
+ testdir.makepyprojecttoml("""
[tool.coverage.run]
patch = ["subprocess"]
""")
@@ -1970,7 +1962,7 @@ def find_labels(text, pattern):
@xdist_params
def test_contexts(pytester, testdir, opts):
- with open(os.path.join(os.path.dirname(__file__), 'contextful.py')) as f:
+ with Path(__file__).parent.joinpath('contextful.py').open() as f:
contextful_tests = f.read()
script = testdir.makepyfile(contextful_tests)
result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-context=test', script, *opts.split())
@@ -1987,7 +1979,7 @@ def test_contexts(pytester, testdir, opts):
measured = data.measured_files()
assert len(measured) == 1
test_context_path = next(iter(measured))
- assert test_context_path.lower() == os.path.abspath('test_contexts.py').lower()
+ assert test_context_path.lower() == os.path.abspath('test_contexts.py').lower() # noqa: PTH100
line_data = find_labels(contextful_tests, r'[crst]\d+(?:-\d+)?')
for context, label in EXPECTED_CONTEXTS.items():
diff --git a/tox.ini b/tox.ini
index 73c7759d..be756012 100644
--- a/tox.ini
+++ b/tox.ini
@@ -10,6 +10,7 @@ passenv =
; a generative tox configuration, see: https://tox.wiki/en/latest/user_guide.html#generative-environments
[tox]
+isolated_build = true
envlist =
clean,
check,
@@ -78,16 +79,14 @@ commands =
[testenv:check]
deps =
docutils
- check-manifest
- pre-commit
- readme-renderer
- pygments
- isort
+ twine
+ uv
+ prek
skip_install = true
commands =
- python setup.py check --strict --metadata --restructuredtext
- check-manifest .
- pre-commit run --all-files --show-diff-on-failure
+ uv build --sdist --wheel
+ twine check --strict dist/*
+ prek run --all-files --show-diff-on-failure
[testenv:docs]
usedevelop = true