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