From 8916e6e95e4e9765bf72d834002dc78949b492f5 Mon Sep 17 00:00:00 2001 From: Mike Manger Date: Sun, 24 Nov 2024 12:24:01 +0000 Subject: [PATCH 01/23] Update CHANGELOG.rst to highlight when python 3.7 and 3.6 where dropped --- CHANGELOG.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d1d60fa3..aa64b55c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -27,6 +27,7 @@ Changelog Contributed by Dawn James in `#626 `_. * Modernized project's pre-commit hooks to use ruff. Initial POC contributed by Christian Clauss in `#584 `_. +* Dropped support for Python 3.7. 4.1.0 (2023-05-24) ------------------ @@ -43,6 +44,7 @@ Changelog Contributed by Mark Mayo in `#572 `_. * Fixed a skip in the test suite for some old xdist. Contributed by a bunch of people in `#565 `_. +* Dropped support for Python 3.6. 4.0.0 (2022-09-28) From b02c9a0a94507eec834bd4785f23ae768c85f99a Mon Sep 17 00:00:00 2001 From: Tsvika Shapira Date: Thu, 20 Mar 2025 14:01:45 +0200 Subject: [PATCH 02/23] fix failing link --- AUTHORS.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index 81e0b8e6..6df96cc7 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -9,7 +9,7 @@ Authors * Marc Abramowitz - \http://marc-abramowitz.com * Thomas Kluyver - https://github.com/takluyver * Guillaume Ayoub - http://www.yabz.fr -* Federico Ceratto - http://firelet.net +* Federico Ceratto - \http://firelet.net * Josh Kalderimis - \http://blog.cookiestack.com * Ionel Cristian Mărieș - https://blog.ionelmc.ro * Christian Ledermann - https://github.com/cleder From 2da9bb7d9bfe3e16a70bcba3e41e74c00a1abe0b Mon Sep 17 00:00:00 2001 From: Tsvika Shapira Date: Thu, 20 Mar 2025 14:18:03 +0200 Subject: [PATCH 03/23] Configure Sphinx to ignore GitHub comment anchors Add linkcheck_anchors_ignore setting to skip GitHub's dynamic comment anchors during documentation link validation. This prevents false positives in CI when checking links to GitHub issue comments, as GitHub injects these anchors dynamically in the web interface. --- docs/conf.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index 0c054b29..69e3300a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -39,6 +39,8 @@ html_split_index = False html_short_title = f'{project}-{version}' +linkcheck_anchors_ignore = ['^issuecomment-'] + napoleon_use_ivar = True napoleon_use_rtype = False napoleon_use_param = False From 72af9ef117ad77b007a4059c641b00ce443c89af Mon Sep 17 00:00:00 2001 From: Tsvika Shapira Date: Thu, 20 Mar 2025 15:23:29 +0200 Subject: [PATCH 04/23] ignore anchors for github --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 69e3300a..488e01bb 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -39,7 +39,7 @@ html_split_index = False html_short_title = f'{project}-{version}' -linkcheck_anchors_ignore = ['^issuecomment-'] +linkcheck_anchors_ignore_for_url = [r'https?://(www\.)?github\.com/.*'] napoleon_use_ivar = True napoleon_use_rtype = False From cc7eca28de371c505c71d8cfdd02ed54e7fbb57d Mon Sep 17 00:00:00 2001 From: Tsvika Shapira Date: Sun, 23 Mar 2025 12:18:39 +0200 Subject: [PATCH 05/23] add ^ to regex MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 🇺🇦 Sviatoslav Sydorenko (Святослав Сидоренко) --- docs/conf.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 488e01bb..4363a76b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -39,7 +39,9 @@ html_split_index = False html_short_title = f'{project}-{version}' -linkcheck_anchors_ignore_for_url = [r'https?://(www\.)?github\.com/.*'] +linkcheck_anchors_ignore_for_url = [ + r'^https?://(www\.)?github\.com/.*', +] napoleon_use_ivar = True napoleon_use_rtype = False From 1e54540d4f2e8494e653af67d50597ee90e538d3 Mon Sep 17 00:00:00 2001 From: Tsvika Shapira Date: Sun, 16 Feb 2025 17:27:19 +0200 Subject: [PATCH 06/23] switch line to _ --- src/pytest_cov/engine.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pytest_cov/engine.py b/src/pytest_cov/engine.py index 650dbcc9..f7896faa 100644 --- a/src/pytest_cov/engine.py +++ b/src/pytest_cov/engine.py @@ -155,15 +155,15 @@ def summary(self, stream): # Output coverage section header. if len(self.node_descs) == 1: - self.sep(stream, '-', f"coverage: {''.join(self.node_descs)}") + self.sep(stream, '_', f"coverage: {''.join(self.node_descs)}") else: - self.sep(stream, '-', 'coverage') + self.sep(stream, '_', 'coverage') for node_desc in sorted(self.node_descs): self.sep(stream, ' ', f'{node_desc}') # Report on any failed workers. if self.failed_workers: - self.sep(stream, '-', 'coverage: failed workers') + self.sep(stream, '_', 'coverage: failed workers') stream.write('The following workers failed to return coverage data, ensure that pytest-cov is installed on these workers.\n') for node in self.failed_workers: stream.write(f'{node.gateway.id}\n') From 102fdc73526db016cc0f0edf7f44cb53e59bae06 Mon Sep 17 00:00:00 2001 From: Tsvika Shapira Date: Sun, 16 Feb 2025 16:35:50 +0200 Subject: [PATCH 07/23] add heading to coverage reports --- src/pytest_cov/plugin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pytest_cov/plugin.py b/src/pytest_cov/plugin.py index 916efc8e..2fbbfd48 100644 --- a/src/pytest_cov/plugin.py +++ b/src/pytest_cov/plugin.py @@ -333,6 +333,7 @@ def pytest_runtestloop(self, session): # it for unit tests that don't need it from coverage.misc import CoverageException + self.cov_controller.sep(self.cov_report, '=', 'coverage report') try: self.cov_total = self.cov_controller.summary(self.cov_report) except CoverageException as exc: From a131cf2da31edcabc5d56792d6be7e674ec73a34 Mon Sep 17 00:00:00 2001 From: Tsvika Shapira Date: Sun, 16 Feb 2025 17:41:16 +0200 Subject: [PATCH 08/23] use TerminalReporter for first heading --- src/pytest_cov/plugin.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/pytest_cov/plugin.py b/src/pytest_cov/plugin.py index 2fbbfd48..69a9ecb1 100644 --- a/src/pytest_cov/plugin.py +++ b/src/pytest_cov/plugin.py @@ -11,6 +11,11 @@ from coverage.results import display_covered from coverage.results import should_fail_under +try: + from pytest import TerminalReporter # noqa: PT013 +except ImportError: + from _pytest.terminal import TerminalReporter + from . import CovDisabledWarning from . import CovFailUnderWarning from . import CovReportWarning @@ -333,7 +338,8 @@ def pytest_runtestloop(self, session): # it for unit tests that don't need it from coverage.misc import CoverageException - self.cov_controller.sep(self.cov_report, '=', 'coverage report') + tr = TerminalReporter(session.config, self.cov_report) + tr.write_sep('=', 'coverage report') try: self.cov_total = self.cov_controller.summary(self.cov_report) except CoverageException as exc: From 43fda5e7966875f69a8984a3defd72ce13880453 Mon Sep 17 00:00:00 2001 From: Tsvika Shapira Date: Sun, 16 Feb 2025 17:29:46 +0200 Subject: [PATCH 09/23] use correct width --- src/pytest_cov/engine.py | 34 ++++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/src/pytest_cov/engine.py b/src/pytest_cov/engine.py index f7896faa..03350f70 100644 --- a/src/pytest_cov/engine.py +++ b/src/pytest_cov/engine.py @@ -5,6 +5,7 @@ import functools import os import random +import shutil import socket import sys import warnings @@ -134,15 +135,36 @@ def get_node_desc(platform, version_info): return 'platform {}, python {}'.format(platform, '{}.{}.{}-{}-{}'.format(*version_info[:5])) @staticmethod - def sep(stream, s, txt): + def get_width(): + width, _ = shutil.get_terminal_size(fallback=(80, 24)) + # The Windows get_terminal_size may be bogus, let's sanify a bit. + if width < 40: + width = 80 + # The goal is to have the line be as long as possible + # under the condition that len(line) <= fullwidth. + if sys.platform == 'win32': + # If we print in the last column on windows we are on a + # new line but there is no way to verify/neutralize this + # (we may not know the exact line width). + # So let's be defensive to avoid empty lines in the output. + width -= 1 + return width + + def sep(self, stream, s, txt): if hasattr(stream, 'sep'): stream.sep(s, txt) else: - sep_total = max((70 - 2 - len(txt)), 2) - sep_len = sep_total // 2 - sep_extra = sep_total % 2 - out = f'{s * sep_len} {txt} {s * (sep_len + sep_extra)}\n' - stream.write(out) + fullwidth = self.get_width() + N = max((fullwidth - len(txt) - 2) // (2 * len(s)), 1) + fill = s * N + line = f'{fill} {txt} {fill}' + # In some situations there is room for an extra sepchar at the right, + # in particular if we consider that with a sepchar like "_ " the + # trailing space is not important at the end of the line. + if len(line) + len(s.rstrip()) <= fullwidth: + line += s.rstrip() + line += '\n' + stream.write(line) @_ensure_topdir def summary(self, stream): From 82d5ea0c82843be454eb75b6922d3dda500122a1 Mon Sep 17 00:00:00 2001 From: Tsvika Shapira Date: Sun, 16 Feb 2025 18:06:02 +0200 Subject: [PATCH 10/23] fix tests for _ --- tests/test_pytest_cov.py | 74 ++++++++++++++++++++-------------------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/tests/test_pytest_cov.py b/tests/test_pytest_cov.py index 8aa2a339..7f3ade38 100644 --- a/tests/test_pytest_cov.py +++ b/tests/test_pytest_cov.py @@ -207,7 +207,7 @@ def test_central(pytester, testdir, prop): result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=term-missing', script, *prop.args) - result.stdout.fnmatch_lines(['*- coverage: platform *, python * -*', f'test_central* {prop.result} *', '*10 passed*']) + result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', f'test_central* {prop.result} *', '*10 passed*']) assert result.ret == 0 @@ -218,7 +218,7 @@ def test_annotate(testdir): result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', 'Coverage annotated source written next to source', '*10 passed*', ] @@ -233,7 +233,7 @@ def test_annotate_output_dir(testdir): result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', 'Coverage annotated source written to dir ' + DEST_DIR, '*10 passed*', ] @@ -251,7 +251,7 @@ def test_html(testdir): result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', 'Coverage HTML written to dir htmlcov', '*10 passed*', ] @@ -269,7 +269,7 @@ def test_html_output_dir(testdir): result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', 'Coverage HTML written to dir ' + DEST_DIR, '*10 passed*', ] @@ -289,7 +289,7 @@ def test_term_report_does_not_interact_with_html_output(testdir): result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', 'Coverage HTML written to dir ' + DEST_DIR, '*1 passed*', ] @@ -317,7 +317,7 @@ def test_html_configured_output_dir(testdir): result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', 'Coverage HTML written to dir somewhere', '*10 passed*', ] @@ -335,7 +335,7 @@ def test_xml_output_dir(testdir): result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', 'Coverage XML written to file ' + XML_REPORT_NAME, '*10 passed*', ] @@ -351,7 +351,7 @@ def test_json_output_dir(testdir): result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', 'Coverage JSON written to file ' + JSON_REPORT_NAME, '*10 passed*', ] @@ -368,7 +368,7 @@ def test_lcov_output_dir(testdir): result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', 'Coverage LCOV written to file ' + LCOV_REPORT_NAME, '*10 passed*', ] @@ -490,7 +490,7 @@ def test_central_nonspecific(pytester, testdir, prop): testdir.tmpdir.join('.coveragerc').write(prop.fullconf) result = testdir.runpytest('-v', '--cov', '--cov-report=term-missing', script, *prop.args) - result.stdout.fnmatch_lines(['*- coverage: platform *, python * -*', f'test_central_nonspecific* {prop.result} *', '*10 passed*']) + result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', f'test_central_nonspecific* {prop.result} *', '*10 passed*']) # multi-module coverage report assert any(line.startswith('TOTAL ') for line in result.stdout.lines) @@ -520,7 +520,7 @@ def test_central_coveragerc(pytester, testdir, prop): result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', f'test_central_coveragerc* {prop.result} *', '*10 passed*', ] @@ -557,7 +557,7 @@ def test_central_with_path_aliasing(pytester, testdir, monkeypatch, opts, prop): result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', f'src[\\/]mod* {prop.result} *', '*10 passed*', ] @@ -599,7 +599,7 @@ def test_foobar(bad): result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', '*mod* 100%', '*1 passed*', ] @@ -636,7 +636,7 @@ def test_subprocess_with_path_aliasing(pytester, testdir, monkeypatch): result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', f'src[\\/]child_script* {CHILD_SCRIPT_RESULT}*', f'src[\\/]parent_script* {PARENT_SCRIPT_RESULT}*', ] @@ -661,7 +661,7 @@ def test_show_missing_coveragerc(pytester, testdir, prop): result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', 'Name * Stmts * Miss * Cover * Missing', f'test_show_missing_coveragerc* {prop.result} * 11*', '*10 passed*', @@ -767,7 +767,7 @@ def test_dist_collocated(pytester, testdir, prop): *prop.args, ) - result.stdout.fnmatch_lines(['*- coverage: platform *, python * -*', f'test_dist_collocated* {prop.result} *', '*10 passed*']) + result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', f'test_dist_collocated* {prop.result} *', '*10 passed*']) assert result.ret == 0 @@ -802,7 +802,7 @@ def test_dist_not_collocated(pytester, testdir, prop): *prop.args, ) - result.stdout.fnmatch_lines(['*- coverage: platform *, python * -*', f'test_dist_not_collocated* {prop.result} *', '*10 passed*']) + result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', f'test_dist_not_collocated* {prop.result} *', '*10 passed*']) assert result.ret == 0 @@ -838,7 +838,7 @@ def test_dist_not_collocated_coveragerc_source(pytester, testdir, prop): *prop.args, ) - result.stdout.fnmatch_lines(['*- coverage: platform *, python * -*', f'test_dist_not_collocated* {prop.result} *', '*10 passed*']) + result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', f'test_dist_not_collocated* {prop.result} *', '*10 passed*']) assert result.ret == 0 @@ -850,7 +850,7 @@ def test_central_subprocess(testdir): result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', f'child_script* {CHILD_SCRIPT_RESULT}*', f'parent_script* {PARENT_SCRIPT_RESULT}*', ] @@ -876,7 +876,7 @@ def test_central_subprocess_change_cwd(testdir): result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', f'*child_script* {CHILD_SCRIPT_RESULT}*', '*parent_script* 100%*', ] @@ -904,7 +904,7 @@ def test_central_subprocess_change_cwd_with_pythonpath(pytester, testdir, monkey result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', f'*child_script* {CHILD_SCRIPT_RESULT}*', ] ) @@ -930,7 +930,7 @@ def test_foo(): result = testdir.runpytest('-v', '--cov-config=coveragerc', f'--cov={script.dirpath()}', '--cov-branch', script) result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', 'test_central_subprocess_no_subscript* * 3 * 0 * 100%*', ] ) @@ -948,7 +948,7 @@ def test_dist_subprocess_collocated(testdir): result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', f'child_script* {CHILD_SCRIPT_RESULT}*', f'parent_script* {PARENT_SCRIPT_RESULT}*', ] @@ -988,7 +988,7 @@ def test_dist_subprocess_not_collocated(pytester, testdir, tmpdir): result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', f'child_script* {CHILD_SCRIPT_RESULT}*', f'parent_script* {PARENT_SCRIPT_RESULT}*', ] @@ -1061,7 +1061,7 @@ def test_funcarg(testdir): result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=term-missing', script) - result.stdout.fnmatch_lines(['*- coverage: platform *, python * -*', 'test_funcarg* 3 * 100%*', '*1 passed*']) + result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', 'test_funcarg* 3 * 100%*', '*1 passed*']) assert result.ret == 0 @@ -1111,7 +1111,7 @@ def test_run(): result = testdir.runpytest('-vv', f'--cov={script.dirpath()}', '--cov-report=term-missing', script) - result.stdout.fnmatch_lines(['*- coverage: platform *, python * -*', 'test_cleanup_on_sigterm* 26-27', '*1 passed*']) + result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', 'test_cleanup_on_sigterm* 26-27', '*1 passed*']) assert result.ret == 0 @@ -1157,7 +1157,7 @@ def test_run(): result = testdir.runpytest('-vv', f'--cov={script.dirpath()}', '--cov-report=term-missing', script) - result.stdout.fnmatch_lines(['*- coverage: platform *, python * -*', f'test_cleanup_on_sigterm* {setup[1]}', '*1 passed*']) + result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', f'test_cleanup_on_sigterm* {setup[1]}', '*1 passed*']) assert result.ret == 0 @@ -1201,7 +1201,7 @@ def test_run(): result = testdir.runpytest('-vv', '--assert=plain', f'--cov={script.dirpath()}', '--cov-report=term-missing', script) - result.stdout.fnmatch_lines(['*- coverage: platform *, python * -*', f'test_cleanup_on_sigterm* {setup[1]}', '*1 passed*']) + result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', f'test_cleanup_on_sigterm* {setup[1]}', '*1 passed*']) assert result.ret == 0 @@ -1236,7 +1236,7 @@ def test_run(): result = testdir.runpytest('-vv', '--assert=plain', f'--cov={script.dirpath()}', '--cov-report=term-missing', script) - result.stdout.fnmatch_lines(['*- coverage: platform *, python * -*', 'test_cleanup_on_sigterm* 88% 19-20', '*1 passed*']) + result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', 'test_cleanup_on_sigterm* 88% 19-20', '*1 passed*']) assert result.ret == 0 @@ -1273,7 +1273,7 @@ def test_run(): result = testdir.runpytest('-vv', '--assert=plain', f'--cov={script.dirpath()}', '--cov-report=term-missing', script) - result.stdout.fnmatch_lines(['*- coverage: platform *, python * -*', 'test_cleanup_on_sigterm* 89% 22-23', '*1 passed*']) + result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', 'test_cleanup_on_sigterm* 89% 22-23', '*1 passed*']) assert result.ret == 0 @@ -1505,7 +1505,7 @@ def test_dist_boxed(testdir): result = testdir.runpytest('-v', '--assert=plain', f'--cov={script.dirpath()}', '--boxed', script) - result.stdout.fnmatch_lines(['*- coverage: platform *, python * -*', f'test_dist_boxed* {SCRIPT_SIMPLE_RESULT}*', '*1 passed*']) + result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', f'test_dist_boxed* {SCRIPT_SIMPLE_RESULT}*', '*1 passed*']) assert result.ret == 0 @@ -1516,7 +1516,7 @@ def test_dist_bare_cov(testdir): result = testdir.runpytest('-v', '--cov', '-n', '1', script) - result.stdout.fnmatch_lines(['*- coverage: platform *, python * -*', f'test_dist_bare_cov* {SCRIPT_SIMPLE_RESULT}*', '*1 passed*']) + result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', f'test_dist_bare_cov* {SCRIPT_SIMPLE_RESULT}*', '*1 passed*']) assert result.ret == 0 @@ -1747,7 +1747,7 @@ def test_append_coverage_subprocess(testdir): result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', f'child_script* {CHILD_SCRIPT_RESULT}*', f'parent_script* {PARENT_SCRIPT_RESULT}*', ] @@ -1781,7 +1781,7 @@ def test_double_cov(testdir): script = testdir.makepyfile(SCRIPT_SIMPLE) result = testdir.runpytest('-v', '--assert=plain', '--cov', f'--cov={script.dirpath()}', script) - result.stdout.fnmatch_lines(['*- coverage: platform *, python * -*', f'test_double_cov* {SCRIPT_SIMPLE_RESULT}*', '*1 passed*']) + result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', f'test_double_cov* {SCRIPT_SIMPLE_RESULT}*', '*1 passed*']) assert result.ret == 0 @@ -1789,7 +1789,7 @@ def test_double_cov2(testdir): script = testdir.makepyfile(SCRIPT_SIMPLE) result = testdir.runpytest('-v', '--assert=plain', '--cov', '--cov', script) - result.stdout.fnmatch_lines(['*- coverage: platform *, python * -*', f'test_double_cov2* {SCRIPT_SIMPLE_RESULT}*', '*1 passed*']) + result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', f'test_double_cov2* {SCRIPT_SIMPLE_RESULT}*', '*1 passed*']) assert result.ret == 0 @@ -1804,7 +1804,7 @@ def test_cov_reset_then_set(testdir): script = testdir.makepyfile(SCRIPT_SIMPLE) result = testdir.runpytest('-v', '--assert=plain', f'--cov={script.dirpath()}', '--cov-reset', f'--cov={script.dirpath()}', script) - result.stdout.fnmatch_lines(['*- coverage: platform *, python * -*', f'test_cov_reset_then_set* {SCRIPT_SIMPLE_RESULT}*', '*1 passed*']) + result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', f'test_cov_reset_then_set* {SCRIPT_SIMPLE_RESULT}*', '*1 passed*']) @pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"') From 22c4ae56afca83bbbbff207513c173e574627c24 Mon Sep 17 00:00:00 2001 From: Tsvika Shapira Date: Thu, 20 Feb 2025 14:36:44 +0200 Subject: [PATCH 11/23] move heading to pytest_terminal_summary --- src/pytest_cov/plugin.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/pytest_cov/plugin.py b/src/pytest_cov/plugin.py index 69a9ecb1..393ef3ae 100644 --- a/src/pytest_cov/plugin.py +++ b/src/pytest_cov/plugin.py @@ -11,11 +11,6 @@ from coverage.results import display_covered from coverage.results import should_fail_under -try: - from pytest import TerminalReporter # noqa: PT013 -except ImportError: - from _pytest.terminal import TerminalReporter - from . import CovDisabledWarning from . import CovFailUnderWarning from . import CovReportWarning @@ -222,6 +217,7 @@ def __init__(self, options, pluginmanager, start=True, no_cov_should_warn=False) self._start_path = None self._disabled = False self.options = options + self.wrote_heading = False is_dist = getattr(options, 'numprocesses', False) or getattr(options, 'distload', False) or getattr(options, 'dist', 'no') != 'no' if getattr(options, 'no_cov', False): @@ -338,8 +334,6 @@ def pytest_runtestloop(self, session): # it for unit tests that don't need it from coverage.misc import CoverageException - tr = TerminalReporter(session.config, self.cov_report) - tr.write_sep('=', 'coverage report') try: self.cov_total = self.cov_controller.summary(self.cov_report) except CoverageException as exc: @@ -363,9 +357,15 @@ def pytest_runtestloop(self, session): # make sure we get the EXIT_TESTSFAILED exit code compat_session.testsfailed += 1 + def write_heading(self, terminalreporter): + if not self.wrote_heading: + terminalreporter.write_sep('=', 'tests coverage') + self.wrote_heading = True + def pytest_terminal_summary(self, terminalreporter): if self._disabled: if self.options.no_cov_should_warn: + self.write_heading(terminalreporter) message = 'Coverage disabled via --no-cov switch!' terminalreporter.write(f'WARNING: {message}\n', red=True, bold=True) warnings.warn(CovDisabledWarning(message), stacklevel=1) @@ -381,9 +381,11 @@ def pytest_terminal_summary(self, terminalreporter): # Avoid undesirable new lines when output is disabled with "--cov-report=". if report: + self.write_heading(terminalreporter) terminalreporter.write('\n' + report + '\n') if self.options.cov_fail_under is not None and self.options.cov_fail_under > 0: + self.write_heading(terminalreporter) failed = self.cov_total < self.options.cov_fail_under markup = {'red': True, 'bold': True} if failed else {'green': True} message = '{fail}Required test coverage of {required}% {reached}. ' 'Total coverage: {actual:.2f}%\n'.format( From 034faf2830ba2f6e955f777b9bfeeca161f33dec Mon Sep 17 00:00:00 2001 From: Tsvika Shapira Date: Thu, 20 Feb 2025 14:36:44 +0200 Subject: [PATCH 12/23] convert newlines to conform with the rest of pytest --- src/pytest_cov/engine.py | 2 +- src/pytest_cov/plugin.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/pytest_cov/engine.py b/src/pytest_cov/engine.py index 03350f70..512e5d88 100644 --- a/src/pytest_cov/engine.py +++ b/src/pytest_cov/engine.py @@ -163,7 +163,7 @@ def sep(self, stream, s, txt): # trailing space is not important at the end of the line. if len(line) + len(s.rstrip()) <= fullwidth: line += s.rstrip() - line += '\n' + line += '\n\n' stream.write(line) @_ensure_topdir diff --git a/src/pytest_cov/plugin.py b/src/pytest_cov/plugin.py index 393ef3ae..f38ade51 100644 --- a/src/pytest_cov/plugin.py +++ b/src/pytest_cov/plugin.py @@ -379,10 +379,9 @@ def pytest_terminal_summary(self, terminalreporter): report = self.cov_report.getvalue() - # Avoid undesirable new lines when output is disabled with "--cov-report=". if report: self.write_heading(terminalreporter) - terminalreporter.write('\n' + report + '\n') + terminalreporter.write(report) if self.options.cov_fail_under is not None and self.options.cov_fail_under > 0: self.write_heading(terminalreporter) From 2a1fed6ecfe895ccba5f08a14ba3a6ecf6aeaec8 Mon Sep 17 00:00:00 2001 From: Tsvika Shapira Date: Thu, 6 Mar 2025 14:46:05 +0200 Subject: [PATCH 13/23] made wrote_headings private --- src/pytest_cov/plugin.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pytest_cov/plugin.py b/src/pytest_cov/plugin.py index f38ade51..011ef704 100644 --- a/src/pytest_cov/plugin.py +++ b/src/pytest_cov/plugin.py @@ -217,7 +217,7 @@ def __init__(self, options, pluginmanager, start=True, no_cov_should_warn=False) self._start_path = None self._disabled = False self.options = options - self.wrote_heading = False + self._wrote_heading = False is_dist = getattr(options, 'numprocesses', False) or getattr(options, 'distload', False) or getattr(options, 'dist', 'no') != 'no' if getattr(options, 'no_cov', False): @@ -358,9 +358,9 @@ def pytest_runtestloop(self, session): compat_session.testsfailed += 1 def write_heading(self, terminalreporter): - if not self.wrote_heading: + if not self._wrote_heading: terminalreporter.write_sep('=', 'tests coverage') - self.wrote_heading = True + self._wrote_heading = True def pytest_terminal_summary(self, terminalreporter): if self._disabled: From 362a35966723a7a51e1cbefb57035b10f12bc65a Mon Sep 17 00:00:00 2001 From: Tsvika Shapira Date: Thu, 6 Mar 2025 15:13:11 +0200 Subject: [PATCH 14/23] move section between functions --- src/pytest_cov/engine.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/pytest_cov/engine.py b/src/pytest_cov/engine.py index 512e5d88..bd2eded2 100644 --- a/src/pytest_cov/engine.py +++ b/src/pytest_cov/engine.py @@ -140,14 +140,6 @@ def get_width(): # The Windows get_terminal_size may be bogus, let's sanify a bit. if width < 40: width = 80 - # The goal is to have the line be as long as possible - # under the condition that len(line) <= fullwidth. - if sys.platform == 'win32': - # If we print in the last column on windows we are on a - # new line but there is no way to verify/neutralize this - # (we may not know the exact line width). - # So let's be defensive to avoid empty lines in the output. - width -= 1 return width def sep(self, stream, s, txt): @@ -155,6 +147,14 @@ def sep(self, stream, s, txt): stream.sep(s, txt) else: fullwidth = self.get_width() + # The goal is to have the line be as long as possible + # under the condition that len(line) <= fullwidth. + if sys.platform == 'win32': + # If we print in the last column on windows we are on a + # new line but there is no way to verify/neutralize this + # (we may not know the exact line width). + # So let's be defensive to avoid empty lines in the output. + fullwidth -= 1 N = max((fullwidth - len(txt) - 2) // (2 * len(s)), 1) fill = s * N line = f'{fill} {txt} {fill}' From 2de0c6c4bd0d4f421d2d599a60ac9826662b22f2 Mon Sep 17 00:00:00 2001 From: Tsvika Shapira Date: Thu, 6 Mar 2025 15:14:00 +0200 Subject: [PATCH 15/23] add reference to code source --- src/pytest_cov/engine.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/pytest_cov/engine.py b/src/pytest_cov/engine.py index bd2eded2..009b96f6 100644 --- a/src/pytest_cov/engine.py +++ b/src/pytest_cov/engine.py @@ -136,6 +136,7 @@ def get_node_desc(platform, version_info): @staticmethod def get_width(): + # taken from https://github.com/pytest-dev/pytest/blob/33c7b05a/src/_pytest/_io/terminalwriter.py#L26 width, _ = shutil.get_terminal_size(fallback=(80, 24)) # The Windows get_terminal_size may be bogus, let's sanify a bit. if width < 40: @@ -147,6 +148,7 @@ def sep(self, stream, s, txt): stream.sep(s, txt) else: fullwidth = self.get_width() + # taken from https://github.com/pytest-dev/pytest/blob/33c7b05a/src/_pytest/_io/terminalwriter.py#L126 # The goal is to have the line be as long as possible # under the condition that len(line) <= fullwidth. if sys.platform == 'win32': @@ -163,6 +165,7 @@ def sep(self, stream, s, txt): # trailing space is not important at the end of the line. if len(line) + len(s.rstrip()) <= fullwidth: line += s.rstrip() + # (end of terminalwriter borrowed code) line += '\n\n' stream.write(line) From ab2cd263ee46ae0777516ac681984c692feff3cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Mon, 24 Mar 2025 11:49:14 +0200 Subject: [PATCH 16/23] Add py 3.13 to test grid and update some deps. --- .github/workflows/test.yml | 90 +++++++++++++++++++++++--------------- tox.ini | 8 ++-- 2 files changed, 59 insertions(+), 39 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 26c8500b..3f6b8dc4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -60,113 +60,131 @@ jobs: toxpython: 'python3.11' tox_env: 'docs' os: 'ubuntu-latest' - - name: 'py39-pytest83-xdist36-coverage76 (ubuntu)' + - name: 'py39-pytest83-xdist36-coverage77 (ubuntu)' python: '3.9' toxpython: 'python3.9' python_arch: 'x64' - tox_env: 'py39-pytest83-xdist36-coverage76' + tox_env: 'py39-pytest83-xdist36-coverage77' os: 'ubuntu-latest' - - name: 'py39-pytest83-xdist36-coverage76 (windows)' + - name: 'py39-pytest83-xdist36-coverage77 (windows)' python: '3.9' toxpython: 'python3.9' python_arch: 'x64' - tox_env: 'py39-pytest83-xdist36-coverage76' + tox_env: 'py39-pytest83-xdist36-coverage77' os: 'windows-latest' - - name: 'py39-pytest83-xdist36-coverage76 (macos)' + - name: 'py39-pytest83-xdist36-coverage77 (macos)' python: '3.9' toxpython: 'python3.9' python_arch: 'arm64' - tox_env: 'py39-pytest83-xdist36-coverage76' + tox_env: 'py39-pytest83-xdist36-coverage77' os: 'macos-latest' - - name: 'py310-pytest83-xdist36-coverage76 (ubuntu)' + - name: 'py310-pytest83-xdist36-coverage77 (ubuntu)' python: '3.10' toxpython: 'python3.10' python_arch: 'x64' - tox_env: 'py310-pytest83-xdist36-coverage76' + tox_env: 'py310-pytest83-xdist36-coverage77' os: 'ubuntu-latest' - - name: 'py310-pytest83-xdist36-coverage76 (windows)' + - name: 'py310-pytest83-xdist36-coverage77 (windows)' python: '3.10' toxpython: 'python3.10' python_arch: 'x64' - tox_env: 'py310-pytest83-xdist36-coverage76' + tox_env: 'py310-pytest83-xdist36-coverage77' os: 'windows-latest' - - name: 'py310-pytest83-xdist36-coverage76 (macos)' + - name: 'py310-pytest83-xdist36-coverage77 (macos)' python: '3.10' toxpython: 'python3.10' python_arch: 'arm64' - tox_env: 'py310-pytest83-xdist36-coverage76' + tox_env: 'py310-pytest83-xdist36-coverage77' os: 'macos-latest' - - name: 'py311-pytest83-xdist36-coverage76 (ubuntu)' + - name: 'py311-pytest83-xdist36-coverage77 (ubuntu)' python: '3.11' toxpython: 'python3.11' python_arch: 'x64' - tox_env: 'py311-pytest83-xdist36-coverage76' + tox_env: 'py311-pytest83-xdist36-coverage77' os: 'ubuntu-latest' - - name: 'py311-pytest83-xdist36-coverage76 (windows)' + - name: 'py311-pytest83-xdist36-coverage77 (windows)' python: '3.11' toxpython: 'python3.11' python_arch: 'x64' - tox_env: 'py311-pytest83-xdist36-coverage76' + tox_env: 'py311-pytest83-xdist36-coverage77' os: 'windows-latest' - - name: 'py311-pytest83-xdist36-coverage76 (macos)' + - name: 'py311-pytest83-xdist36-coverage77 (macos)' python: '3.11' toxpython: 'python3.11' python_arch: 'arm64' - tox_env: 'py311-pytest83-xdist36-coverage76' + tox_env: 'py311-pytest83-xdist36-coverage77' os: 'macos-latest' - - name: 'py312-pytest83-xdist36-coverage76 (ubuntu)' + - name: 'py312-pytest83-xdist36-coverage77 (ubuntu)' python: '3.12' toxpython: 'python3.12' python_arch: 'x64' - tox_env: 'py312-pytest83-xdist36-coverage76' + tox_env: 'py312-pytest83-xdist36-coverage77' os: 'ubuntu-latest' - - name: 'py312-pytest83-xdist36-coverage76 (windows)' + - name: 'py312-pytest83-xdist36-coverage77 (windows)' python: '3.12' toxpython: 'python3.12' python_arch: 'x64' - tox_env: 'py312-pytest83-xdist36-coverage76' + tox_env: 'py312-pytest83-xdist36-coverage77' os: 'windows-latest' - - name: 'py312-pytest83-xdist36-coverage76 (macos)' + - name: 'py312-pytest83-xdist36-coverage77 (macos)' python: '3.12' toxpython: 'python3.12' python_arch: 'arm64' - tox_env: 'py312-pytest83-xdist36-coverage76' + tox_env: 'py312-pytest83-xdist36-coverage77' os: 'macos-latest' - - name: 'pypy39-pytest83-xdist36-coverage76 (ubuntu)' + - name: 'py313-pytest83-xdist36-coverage77 (ubuntu)' + python: '3.13' + toxpython: 'python3.13' + python_arch: 'x64' + tox_env: 'py313-pytest83-xdist36-coverage77' + os: 'ubuntu-latest' + - name: 'py313-pytest83-xdist36-coverage77 (windows)' + python: '3.13' + toxpython: 'python3.13' + python_arch: 'x64' + tox_env: 'py313-pytest83-xdist36-coverage77' + os: 'windows-latest' + - name: 'py313-pytest83-xdist36-coverage77 (macos)' + python: '3.13' + toxpython: 'python3.13' + python_arch: 'arm64' + tox_env: 'py313-pytest83-xdist36-coverage77' + os: 'macos-latest' + - name: 'pypy39-pytest83-xdist36-coverage77 (ubuntu)' python: 'pypy-3.9' toxpython: 'pypy3.9' python_arch: 'x64' - tox_env: 'pypy39-pytest83-xdist36-coverage76' + tox_env: 'pypy39-pytest83-xdist36-coverage77' os: 'ubuntu-latest' - - name: 'pypy39-pytest83-xdist36-coverage76 (windows)' + - name: 'pypy39-pytest83-xdist36-coverage77 (windows)' python: 'pypy-3.9' toxpython: 'pypy3.9' python_arch: 'x64' - tox_env: 'pypy39-pytest83-xdist36-coverage76' + tox_env: 'pypy39-pytest83-xdist36-coverage77' os: 'windows-latest' - - name: 'pypy39-pytest83-xdist36-coverage76 (macos)' + - name: 'pypy39-pytest83-xdist36-coverage77 (macos)' python: 'pypy-3.9' toxpython: 'pypy3.9' python_arch: 'arm64' - tox_env: 'pypy39-pytest83-xdist36-coverage76' + tox_env: 'pypy39-pytest83-xdist36-coverage77' os: 'macos-latest' - - name: 'pypy310-pytest83-xdist36-coverage76 (ubuntu)' + - name: 'pypy310-pytest83-xdist36-coverage77 (ubuntu)' python: 'pypy-3.10' toxpython: 'pypy3.10' python_arch: 'x64' - tox_env: 'pypy310-pytest83-xdist36-coverage76' + tox_env: 'pypy310-pytest83-xdist36-coverage77' os: 'ubuntu-latest' - - name: 'pypy310-pytest83-xdist36-coverage76 (windows)' + - name: 'pypy310-pytest83-xdist36-coverage77 (windows)' python: 'pypy-3.10' toxpython: 'pypy3.10' python_arch: 'x64' - tox_env: 'pypy310-pytest83-xdist36-coverage76' + tox_env: 'pypy310-pytest83-xdist36-coverage77' os: 'windows-latest' - - name: 'pypy310-pytest83-xdist36-coverage76 (macos)' + - name: 'pypy310-pytest83-xdist36-coverage77 (macos)' python: 'pypy-3.10' toxpython: 'pypy3.10' python_arch: 'arm64' - tox_env: 'pypy310-pytest83-xdist36-coverage76' + tox_env: 'pypy310-pytest83-xdist36-coverage77' os: 'macos-latest' steps: - uses: actions/checkout@v4 diff --git a/tox.ini b/tox.ini index 5dc537e3..3880ea27 100644 --- a/tox.ini +++ b/tox.ini @@ -14,7 +14,7 @@ envlist = clean, check, docs, - {py39,py310,py311,py312,pypy39,pypy310}-{pytest83}-{xdist36}-{coverage76}, + {py39,py310,py311,py312,py313,pypy39,pypy310}-{pytest83}-{xdist36}-{coverage77}, report ignore_basepython_conflict = true @@ -28,6 +28,7 @@ basepython = py310: {env:TOXPYTHON:python3.10} py311: {env:TOXPYTHON:python3.11} py312: {env:TOXPYTHON:python3.12} + py313: {env:TOXPYTHON:python3.13} {bootstrap,clean,check,report,docs}: {env:TOXPYTHON:python3} extras = testing setenv = @@ -38,7 +39,7 @@ setenv = pytest80: _DEP_PYTEST=pytest==8.0.2 pytest81: _DEP_PYTEST=pytest==8.1.1 pytest82: _DEP_PYTEST=pytest==8.2.2 - pytest83: _DEP_PYTEST=pytest==8.3.3 + pytest83: _DEP_PYTEST=pytest==8.3.5 xdist32: _DEP_PYTESTXDIST=pytest-xdist==3.2.0 xdist33: _DEP_PYTESTXDIST=pytest-xdist==3.3.1 @@ -51,7 +52,8 @@ setenv = coverage73: _DEP_COVERAGE=coverage==7.3.4 coverage74: _DEP_COVERAGE=coverage==7.4.4 coverage75: _DEP_COVERAGE=coverage==7.5.4 - coverage76: _DEP_COVERAGE=coverage==7.6.4 + coverage76: _DEP_COVERAGE=coverage==7.6.12 + coverage77: _DEP_COVERAGE=coverage==7.7.1 # For testing against a coverage.py working tree. coveragedev: _DEP_COVERAGE=-e{env:COVERAGE_HOME} passenv = From 089e7bb5c16dcfdedd54f27fda094ccb3eeaae2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Mon, 24 Mar 2025 11:50:46 +0200 Subject: [PATCH 17/23] Upgrade ruff. --- .pre-commit-config.yaml | 4 ++-- setup.py | 2 +- src/pytest_cov/engine.py | 4 ++-- src/pytest_cov/plugin.py | 7 +++---- tests/test_pytest_cov.py | 4 ++-- 5 files changed, 10 insertions(+), 11 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6bc8d8cd..4cd7d3e5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,13 +6,13 @@ 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.6.5 + rev: v0.11.2 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix, --show-fixes, --unsafe-fixes] - id: ruff-format - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + rev: v5.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer diff --git a/setup.py b/setup.py index bc520b80..cc71d903 100755 --- a/setup.py +++ b/setup.py @@ -72,7 +72,7 @@ def finalize_options(self): def run(self): with Path(__file__).parent.joinpath('src', 'pytest-cov.pth').open('w') as fh: with Path(__file__).parent.joinpath('src', 'pytest-cov.embed').open() as sh: - fh.write(f"import os, sys;exec({sh.read().replace(' ', ' ')!r})") + fh.write(f'import os, sys;exec({sh.read().replace(" ", " ")!r})') setup( diff --git a/src/pytest_cov/engine.py b/src/pytest_cov/engine.py index 009b96f6..826205d7 100644 --- a/src/pytest_cov/engine.py +++ b/src/pytest_cov/engine.py @@ -180,7 +180,7 @@ def summary(self, stream): # Output coverage section header. if len(self.node_descs) == 1: - self.sep(stream, '_', f"coverage: {''.join(self.node_descs)}") + self.sep(stream, '_', f'coverage: {"".join(self.node_descs)}') else: self.sep(stream, '_', 'coverage') for node_desc in sorted(self.node_descs): @@ -366,7 +366,7 @@ def testnodedown(self, node, error): # If worker is not collocated then we must save the data file # that it returns to us. if 'cov_worker_data' in output: - data_suffix = '%s.%s.%06d.%s' % ( + data_suffix = '%s.%s.%06d.%s' % ( # noqa: UP031 socket.gethostname(), os.getpid(), random.randint(0, 999999), # noqa: S311 diff --git a/src/pytest_cov/plugin.py b/src/pytest_cov/plugin.py index 011ef704..f28907b6 100644 --- a/src/pytest_cov/plugin.py +++ b/src/pytest_cov/plugin.py @@ -56,8 +56,7 @@ def validate_fail_under(num_str): raise argparse.ArgumentTypeError('An integer or float value is required.') from None if value > 100: raise argparse.ArgumentTypeError( - 'Your desire for over-achievement is admirable but misplaced. ' - 'The maximum value is 100. Perhaps write more integration tests?' + 'Your desire for over-achievement is admirable but misplaced. The maximum value is 100. Perhaps write more integration tests?' ) return value @@ -387,7 +386,7 @@ def pytest_terminal_summary(self, terminalreporter): self.write_heading(terminalreporter) failed = self.cov_total < self.options.cov_fail_under markup = {'red': True, 'bold': True} if failed else {'green': True} - message = '{fail}Required test coverage of {required}% {reached}. ' 'Total coverage: {actual:.2f}%\n'.format( + message = '{fail}Required test coverage of {required}% {reached}. Total coverage: {actual:.2f}%\n'.format( required=self.options.cov_fail_under, actual=self.cov_total, fail='FAIL ' if failed else '', @@ -434,7 +433,7 @@ def switch_context(self, item, when): @pytest.fixture -def no_cover(): # noqa: PT004 +def no_cover(): """A pytest fixture to disable coverage.""" diff --git a/tests/test_pytest_cov.py b/tests/test_pytest_cov.py index 7f3ade38..c93498e5 100644 --- a/tests/test_pytest_cov.py +++ b/tests/test_pytest_cov.py @@ -414,7 +414,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, + '*argument --cov-report: output specifier not supported for: "term-missing:%s"*' % DEST_DIR, ] ) assert result.ret != 0 @@ -1773,7 +1773,7 @@ def bad_init(): monkeypatch.setattr(sys, 'stderr', buff) monkeypatch.setitem(os.environ, 'COV_CORE_SOURCE', 'foobar') exec(payload) - expected = 'pytest-cov: Failed to setup subprocess coverage. ' "Environ: {'COV_CORE_SOURCE': 'foobar'} Exception: SpecificError()\n" + expected = "pytest-cov: Failed to setup subprocess coverage. Environ: {'COV_CORE_SOURCE': 'foobar'} Exception: SpecificError()\n" assert buff.getvalue() == expected From 204af146f8f4ff03076825a693ee6aef587deb6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Mon, 24 Mar 2025 11:54:16 +0200 Subject: [PATCH 18/23] Update changelog. --- AUTHORS.rst | 1 + CHANGELOG.rst | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/AUTHORS.rst b/AUTHORS.rst index 6df96cc7..5a2dc63d 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -62,3 +62,4 @@ Authors * Matthew Gamble - https://github.com/mwgamble * Christian Clauss - https://github.com/cclauss * Dawn James - https://github.com/dawngerpony +* Tsvika Shapira - https://github.com/tsvikas diff --git a/CHANGELOG.rst b/CHANGELOG.rst index aa64b55c..33298ac2 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,12 @@ Changelog ========= +6.1.0 (2025-03-2?) +------------------ + +* Change terminal output to use full width lines for the coverage header. + Contributed by Tsvika Shapira in `#678 `_. + 6.0.0 (2024-10-29) ------------------ From 44540e1e9f02f3b69b62834636cf3057edc960d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Mon, 31 Mar 2025 13:21:40 +0300 Subject: [PATCH 19/23] Remove unnecessary CovFailUnderWarning. Closes #675. --- src/pytest_cov/__init__.py | 6 ------ src/pytest_cov/plugin.py | 2 -- 2 files changed, 8 deletions(-) diff --git a/src/pytest_cov/__init__.py b/src/pytest_cov/__init__.py index 4caae9dc..ef5bf7a5 100644 --- a/src/pytest_cov/__init__.py +++ b/src/pytest_cov/__init__.py @@ -27,12 +27,6 @@ class CovReportWarning(PytestCovWarning): """ -class CovFailUnderWarning(PytestCovWarning): - """ - Indicates that we failed to generate a report. - """ - - class CentralCovContextWarning(PytestCovWarning): """ Indicates that dynamic_context was set to test_function instead of using the builtin --cov-context. diff --git a/src/pytest_cov/plugin.py b/src/pytest_cov/plugin.py index f28907b6..376d876c 100644 --- a/src/pytest_cov/plugin.py +++ b/src/pytest_cov/plugin.py @@ -12,7 +12,6 @@ from coverage.results import should_fail_under from . import CovDisabledWarning -from . import CovFailUnderWarning from . import CovReportWarning from . import compat from . import embed @@ -352,7 +351,6 @@ def pytest_runtestloop(self, session): p=cov_precision, ) session.config.pluginmanager.getplugin('terminalreporter').write(f'\nERROR: {message}\n', red=True, bold=True) - warnings.warn(CovFailUnderWarning(message), stacklevel=1) # make sure we get the EXIT_TESTSFAILED exit code compat_session.testsfailed += 1 From e760099a7fd5f49c235dc798bf7f222c0372b7e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Mon, 31 Mar 2025 13:39:44 +0300 Subject: [PATCH 20/23] Make sure the CLI precision is used when creating report. Fixes #674. --- src/pytest_cov/engine.py | 6 +++++- src/pytest_cov/plugin.py | 7 ++++++- tests/test_pytest_cov.py | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 2 deletions(-) diff --git a/src/pytest_cov/engine.py b/src/pytest_cov/engine.py index 826205d7..83efc702 100644 --- a/src/pytest_cov/engine.py +++ b/src/pytest_cov/engine.py @@ -67,13 +67,16 @@ def _data_suffix(name): class CovController: """Base class for different plugin implementations.""" - def __init__(self, cov_source, cov_report, cov_config, cov_append, cov_branch, config=None, nodeid=None): + cov: coverage.Coverage | None + + def __init__(self, cov_source, cov_report, cov_config, cov_append, cov_branch, cov_precision, config=None, nodeid=None): """Get some common config used by multiple derived classes.""" self.cov_source = cov_source self.cov_report = cov_report self.cov_config = cov_config self.cov_append = cov_append self.cov_branch = cov_branch + self.cov_precision = cov_precision self.config = config self.nodeid = nodeid @@ -199,6 +202,7 @@ def summary(self, stream): 'show_missing': ('term-missing' in self.cov_report) or None, 'ignore_errors': True, 'file': stream, + 'precision': self.cov_precision, } skip_covered = isinstance(self.cov_report, dict) and 'skip-covered' in self.cov_report.values() options.update({'skip_covered': skip_covered or None}) diff --git a/src/pytest_cov/plugin.py b/src/pytest_cov/plugin.py index 376d876c..9eced06d 100644 --- a/src/pytest_cov/plugin.py +++ b/src/pytest_cov/plugin.py @@ -5,6 +5,7 @@ import warnings from io import StringIO from pathlib import Path +from typing import TYPE_CHECKING import coverage import pytest @@ -16,6 +17,9 @@ from . import compat from . import embed +if TYPE_CHECKING: + from .engine import CovController + def validate_report(arg): file_choices = ['annotate', 'html', 'xml', 'json', 'lcov'] @@ -239,7 +243,7 @@ def __init__(self, options, pluginmanager, start=True, no_cov_should_warn=False) # worker is started in pytest hook - def start(self, controller_cls, config=None, nodeid=None): + def start(self, controller_cls: 'CovController', config=None, nodeid=None): if config is None: # fake config option for engine class Config: @@ -253,6 +257,7 @@ class Config: self.options.cov_config, self.options.cov_append, self.options.cov_branch, + self.options.cov_precision, config, nodeid, ) diff --git a/tests/test_pytest_cov.py b/tests/test_pytest_cov.py index c93498e5..81184f8b 100644 --- a/tests/test_pytest_cov.py +++ b/tests/test_pytest_cov.py @@ -476,6 +476,39 @@ def test_cov_min_float_value_not_reached_cli(testdir): result.stdout.fnmatch_lines(['FAIL Required test coverage of 88.89% not reached. Total coverage: 88.89%']) +def test_cov_precision(testdir): + script = testdir.makepyfile(SCRIPT) + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=term-missing', '--cov-precision=6', script) + assert result.ret == 0 + result.stdout.fnmatch_lines( + [ + 'Name Stmts Miss Cover Missing', + '----------------------------------------------------------', + 'test_cov_precision.py 9 1 88.888889% 11', + '----------------------------------------------------------', + 'TOTAL 9 1 88.888889%', + ] + ) + + +def test_cov_precision_from_config(testdir): + script = testdir.makepyfile(SCRIPT) + testdir.tmpdir.join('pyproject.toml').write(""" +[tool.coverage.report] +precision = 6""") + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=term-missing', script) + assert result.ret == 0 + result.stdout.fnmatch_lines( + [ + 'Name Stmts Miss Cover Missing', + '----------------------------------------------------------------------', + 'test_cov_precision_from_config.py 9 1 88.888889% 11', + '----------------------------------------------------------------------', + 'TOTAL 9 1 88.888889%', + ] + ) + + def test_cov_min_no_report(testdir): script = testdir.makepyfile(SCRIPT) From aa57aed273475b4f9975cc9a8a1662b336718662 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Tue, 1 Apr 2025 05:00:09 +0300 Subject: [PATCH 21/23] Refactor a bit the internals to be a bit less boilerplatey and have more clarity. The config/nodeid were never meant to be optional for this internal class - the intention there was to signal that they can be None values. --- src/pytest_cov/engine.py | 18 +++++++++--------- src/pytest_cov/plugin.py | 15 +++------------ 2 files changed, 12 insertions(+), 21 deletions(-) diff --git a/src/pytest_cov/engine.py b/src/pytest_cov/engine.py index 83efc702..5acafec0 100644 --- a/src/pytest_cov/engine.py +++ b/src/pytest_cov/engine.py @@ -1,5 +1,6 @@ """Coverage controllers for use by pytest-cov and nose-cov.""" +import argparse import contextlib import copy import functools @@ -11,6 +12,7 @@ import warnings from io import StringIO from pathlib import Path +from typing import Union import coverage from coverage.data import CoverageData @@ -67,16 +69,14 @@ def _data_suffix(name): class CovController: """Base class for different plugin implementations.""" - cov: coverage.Coverage | None - - def __init__(self, cov_source, cov_report, cov_config, cov_append, cov_branch, cov_precision, config=None, nodeid=None): + def __init__(self, options: argparse.Namespace, config: Union[None, object], nodeid: Union[None, str]): """Get some common config used by multiple derived classes.""" - self.cov_source = cov_source - self.cov_report = cov_report - self.cov_config = cov_config - self.cov_append = cov_append - self.cov_branch = cov_branch - self.cov_precision = cov_precision + self.cov_source = options.cov_source + self.cov_report = options.cov_report + self.cov_config = options.cov_config + self.cov_append = options.cov_append + self.cov_branch = options.cov_branch + self.cov_precision = options.cov_precision self.config = config self.nodeid = nodeid diff --git a/src/pytest_cov/plugin.py b/src/pytest_cov/plugin.py index 9eced06d..b131742b 100644 --- a/src/pytest_cov/plugin.py +++ b/src/pytest_cov/plugin.py @@ -201,7 +201,7 @@ class CovPlugin: distributed worker. """ - def __init__(self, options, pluginmanager, start=True, no_cov_should_warn=False): + def __init__(self, options: argparse.Namespace, pluginmanager, start=True, no_cov_should_warn=False): """Creates a coverage pytest plugin. We read the rc file that coverage uses to get the data file @@ -243,7 +243,7 @@ def __init__(self, options, pluginmanager, start=True, no_cov_should_warn=False) # worker is started in pytest hook - def start(self, controller_cls: 'CovController', config=None, nodeid=None): + def start(self, controller_cls: type['CovController'], config=None, nodeid=None): if config is None: # fake config option for engine class Config: @@ -251,16 +251,7 @@ class Config: config = Config() - self.cov_controller = controller_cls( - self.options.cov_source, - self.options.cov_report, - self.options.cov_config, - self.options.cov_append, - self.options.cov_branch, - self.options.cov_precision, - config, - nodeid, - ) + self.cov_controller = controller_cls(self.options, config, nodeid) self.cov_controller.start() self._started = True self._start_path = Path.cwd() From 10b14afffcd53b19967785c0b3e8b35ebac70b6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Tue, 1 Apr 2025 13:54:44 +0300 Subject: [PATCH 22/23] Update changelog. --- CHANGELOG.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 33298ac2..7de457fd 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,11 +2,14 @@ Changelog ========= -6.1.0 (2025-03-2?) +6.1.0 (2025-04-01) ------------------ * Change terminal output to use full width lines for the coverage header. Contributed by Tsvika Shapira in `#678 `_. +* Removed unnecessary CovFailUnderWarning. Fixes `#675 `_. +* Fixed the term report not using the precision specified via ``--cov-precision``. + 6.0.0 (2024-10-29) ------------------ From 10f8cde38c3b0aaf2c75d9ed62d4f333d8809d96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Tue, 1 Apr 2025 13:55:06 +0300 Subject: [PATCH 23/23] =?UTF-8?q?Bump=20version:=206.0.0=20=E2=86=92=206.1?= =?UTF-8?q?.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- .cookiecutterrc | 2 +- README.rst | 4 ++-- docs/conf.py | 2 +- setup.py | 2 +- src/pytest_cov/__init__.py | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 4f2843c8..98e4fe0e 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 6.0.0 +current_version = 6.1.0 commit = True tag = True diff --git a/.cookiecutterrc b/.cookiecutterrc index 1a6fdf34..46b134a9 100644 --- a/.cookiecutterrc +++ b/.cookiecutterrc @@ -40,7 +40,7 @@ default_context: sphinx_doctest: 'no' sphinx_theme: sphinx-py3doc-enhanced-theme test_matrix_separate_coverage: 'no' - version: 6.0.0 + version: 6.1.0 version_manager: bump2version website: http://blog.ionelmc.ro year_from: '2010' diff --git a/README.rst b/README.rst index d7af95a4..d0b3464d 100644 --- a/README.rst +++ b/README.rst @@ -39,9 +39,9 @@ Overview :alt: Supported implementations :target: https://pypi.org/project/pytest-cov -.. |commits-since| image:: https://img.shields.io/github/commits-since/pytest-dev/pytest-cov/v6.0.0.svg +.. |commits-since| image:: https://img.shields.io/github/commits-since/pytest-dev/pytest-cov/v6.1.0.svg :alt: Commits since latest release - :target: https://github.com/pytest-dev/pytest-cov/compare/v6.0.0...master + :target: https://github.com/pytest-dev/pytest-cov/compare/v6.1.0...master .. end-badges diff --git a/docs/conf.py b/docs/conf.py index 4363a76b..f73bf5c2 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -21,7 +21,7 @@ year = '2010-2024' author = 'pytest-cov contributors' copyright = f'{year}, {author}' -version = release = '6.0.0' +version = release = '6.1.0' pygments_style = 'trac' templates_path = ['.'] diff --git a/setup.py b/setup.py index cc71d903..d773a455 100755 --- a/setup.py +++ b/setup.py @@ -77,7 +77,7 @@ def run(self): setup( name='pytest-cov', - version='6.0.0', + version='6.1.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'))), diff --git a/src/pytest_cov/__init__.py b/src/pytest_cov/__init__.py index ef5bf7a5..716bc6a9 100644 --- a/src/pytest_cov/__init__.py +++ b/src/pytest_cov/__init__.py @@ -1,6 +1,6 @@ """pytest-cov: avoid already-imported warning: PYTEST_DONT_REWRITE.""" -__version__ = '6.0.0' +__version__ = '6.1.0' import pytest