From aa55dbaddfce0ec3a053882f690e81b91c010371 Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Wed, 27 Dec 2023 17:25:39 -0500
Subject: [PATCH 01/41] build: bump version
---
CHANGES.rst | 6 ++++++
coverage/version.py | 4 ++--
2 files changed, 8 insertions(+), 2 deletions(-)
diff --git a/CHANGES.rst b/CHANGES.rst
index a8ca7bc40..fc0dfcbfc 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -17,6 +17,12 @@ development at the same time, such as 4.5.x and 5.0.
.. Version 9.8.1 — 2027-07-27
.. --------------------------
+Unreleased
+----------
+
+Nothing yet.
+
+
.. scriv-start-here
.. _changes_7-4-0:
diff --git a/coverage/version.py b/coverage/version.py
index 00865c9f5..b8541c8b7 100644
--- a/coverage/version.py
+++ b/coverage/version.py
@@ -8,8 +8,8 @@
# version_info: same semantics as sys.version_info.
# _dev: the .devN suffix if any.
-version_info = (7, 4, 0, "final", 0)
-_dev = 0
+version_info = (7, 4, 1, "alpha", 0)
+_dev = 1
def _make_version(
From a94f21ea541e49dd6815d96195d52d818e329174 Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Fri, 29 Dec 2023 10:28:46 -0500
Subject: [PATCH 02/41] docs: clarify the 7.4.0 release
---
CHANGES.rst | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/CHANGES.rst b/CHANGES.rst
index fc0dfcbfc..59071a8ee 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -32,9 +32,10 @@ Version 7.4.0 — 2023-12-27
- In Python 3.12 and above, you can try an experimental core based on the new
:mod:`sys.monitoring ` module by defining a
- ``COVERAGE_CORE=sysmon`` environment variable. This should be faster, though
- plugins and dynamic contexts are not yet supported with it. I am very
- interested to hear how it works (or doesn't!) for you.
+ ``COVERAGE_CORE=sysmon`` environment variable. This should be faster for
+ line coverage, but not for branch coverage, and plugins and dynamic contexts
+ are not yet supported with it. I am very interested to hear how it works (or
+ doesn't!) for you.
.. _changes_7-3-4:
From cf75d0223415ee5feefc29ad000ea40505386a69 Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Tue, 2 Jan 2024 06:04:34 -0500
Subject: [PATCH 03/41] test: show failed warnings better
---
tests/helpers.py | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/tests/helpers.py b/tests/helpers.py
index bd7ea184f..9e6e2e8de 100644
--- a/tests/helpers.py
+++ b/tests/helpers.py
@@ -330,8 +330,9 @@ def assert_coverage_warnings(
"""
assert msgs # don't call this without some messages.
warns = [w for w in warns if issubclass(w.category, CoverageWarning)]
- assert len(warns) == len(msgs)
- for actual, expected in zip((cast(Warning, w.message).args[0] for w in warns), msgs):
+ actuals = [cast(Warning, w.message).args[0] for w in warns]
+ assert len(msgs) == len(actuals)
+ for expected, actual in zip(msgs, actuals):
if hasattr(expected, "search"):
assert expected.search(actual), f"{actual!r} didn't match {expected!r}"
else:
From 0722f02a30f0a2cbc1a7e7d7613f9fd983c14830 Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Mon, 1 Jan 2024 05:51:07 -0500
Subject: [PATCH 04/41] debug: easier pytrace logging, smaller recursion test
---
coverage/pytracer.py | 8 +++++++-
tests/test_oddball.py | 13 +++++++------
2 files changed, 14 insertions(+), 7 deletions(-)
diff --git a/coverage/pytracer.py b/coverage/pytracer.py
index 789ffad00..de647c0cb 100644
--- a/coverage/pytracer.py
+++ b/coverage/pytracer.py
@@ -7,6 +7,7 @@
import atexit
import dis
+import itertools
import sys
import threading
@@ -51,7 +52,12 @@ class PyTracer(TracerCore):
# PyTracer to get accurate results. The command-line --timid argument is
# used to force the use of this tracer.
+ tracer_ids = itertools.count()
+
def __init__(self) -> None:
+ # Which tracer are we?
+ self.id = next(self.tracer_ids)
+
# Attributes set from the collector:
self.data: TTraceData
self.trace_arcs = False
@@ -101,7 +107,7 @@ def log(self, marker: str, *args: Any) -> None:
with open("/tmp/debug_trace.txt", "a") as f:
f.write("{} {}[{}]".format(
marker,
- id(self),
+ self.id,
len(self.data_stack),
))
if 0: # if you want thread ids..
diff --git a/tests/test_oddball.py b/tests/test_oddball.py
index f26bc918c..d1decd41c 100644
--- a/tests/test_oddball.py
+++ b/tests/test_oddball.py
@@ -112,6 +112,7 @@ def test_long_recursion_recovery(self) -> None:
# will be traced.
self.make_file("recur.py", """\
+ import sys #; sys.setrecursionlimit(70)
def recur(n):
if n == 0:
return 0 # never hit
@@ -121,8 +122,8 @@ def recur(n):
try:
recur(100000) # This is definitely too many frames.
except RuntimeError:
- i = 10
- i = 11
+ i = 11
+ i = 12
""")
cov = coverage.Coverage()
@@ -131,12 +132,12 @@ def recur(n):
assert cov._collector is not None
pytrace = (cov._collector.tracer_name() == "PyTracer")
- expected_missing = [3]
+ expected_missing = [4]
if pytrace: # pragma: no metacov
- expected_missing += [9, 10, 11]
+ expected_missing += [10, 11, 12]
_, statements, missing, _ = cov.analysis("recur.py")
- assert statements == [1, 2, 3, 5, 7, 8, 9, 10, 11]
+ assert statements == [1, 2, 3, 4, 6, 8, 9, 10, 11, 12]
assert expected_missing == missing
# Get a warning about the stackoverflow effect on the tracing function.
@@ -145,7 +146,7 @@ def recur(n):
assert re.fullmatch(
r"Trace function changed, data is likely wrong: None != " +
r">",
+ ">",
cov._warnings[0],
)
else:
From c254cce51fcdfcda1ec3a18ae660af210713370d Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Mon, 1 Jan 2024 10:11:29 -0500
Subject: [PATCH 05/41] fix: defend against missing last_line
---
coverage/sysmon.py | 18 ++++++++++++------
1 file changed, 12 insertions(+), 6 deletions(-)
diff --git a/coverage/sysmon.py b/coverage/sysmon.py
index f2f42c777..e16e49d6e 100644
--- a/coverage/sysmon.py
+++ b/coverage/sysmon.py
@@ -377,8 +377,10 @@ def sysmon_py_return_arcs(
frame = self.callers_frame()
code_info = self.code_infos.get(id(code))
if code_info is not None and code_info.file_data is not None:
- arc = (self.last_lines[frame], -code.co_firstlineno)
- cast(Set[TArc], code_info.file_data).add(arc)
+ last_line = self.last_lines.get(frame)
+ if last_line is not None:
+ arc = (last_line, -code.co_firstlineno)
+ cast(Set[TArc], code_info.file_data).add(arc)
# Leaving this function, no need for the frame any more.
self.last_lines.pop(frame, None)
@@ -391,8 +393,10 @@ def sysmon_py_unwind_arcs(
frame = self.callers_frame()
code_info = self.code_infos.get(id(code))
if code_info is not None and code_info.file_data is not None:
- arc = (self.last_lines[frame], -code.co_firstlineno)
- cast(Set[TArc], code_info.file_data).add(arc)
+ last_line = self.last_lines.get(frame)
+ if last_line is not None:
+ arc = (last_line, -code.co_firstlineno)
+ cast(Set[TArc], code_info.file_data).add(arc)
# Leaving this function.
self.last_lines.pop(frame, None)
@@ -413,8 +417,10 @@ def sysmon_line_arcs(self, code: CodeType, line_number: int) -> MonitorReturn:
ret = None
if code_info.file_data is not None:
frame = self.callers_frame()
- arc = (self.last_lines[frame], line_number)
- cast(Set[TArc], code_info.file_data).add(arc)
+ last_line = self.last_lines.get(frame)
+ if last_line is not None:
+ arc = (last_line, line_number)
+ cast(Set[TArc], code_info.file_data).add(arc)
# log(f"adding {arc=}")
self.last_lines[frame] = line_number
return ret
From 42fa1aeef50a8aa468025cc9cd28cd05e3f1ec36 Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Tue, 2 Jan 2024 17:34:53 -0500
Subject: [PATCH 06/41] fix(debug): close our debug file at process end
---
coverage/debug.py | 2 ++
1 file changed, 2 insertions(+)
diff --git a/coverage/debug.py b/coverage/debug.py
index 072a37bd8..000a2cab9 100644
--- a/coverage/debug.py
+++ b/coverage/debug.py
@@ -5,6 +5,7 @@
from __future__ import annotations
+import atexit
import contextlib
import functools
import inspect
@@ -450,6 +451,7 @@ def get_one(
fileobj = getattr(sys, file_name)
elif file_name:
fileobj = open(file_name, "a", encoding="utf-8")
+ atexit.register(fileobj.close)
else:
fileobj = sys.stderr
the_one = cls(fileobj, filters)
From bee1012a4c94b87ce803a4b243c7d5a0434fe5a3 Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Tue, 2 Jan 2024 18:07:33 -0500
Subject: [PATCH 07/41] refactor: remove a pytrace vestige from sysmon
---
coverage/sysmon.py | 5 -----
1 file changed, 5 deletions(-)
diff --git a/coverage/sysmon.py b/coverage/sysmon.py
index e16e49d6e..0d42ac483 100644
--- a/coverage/sysmon.py
+++ b/coverage/sysmon.py
@@ -5,7 +5,6 @@
from __future__ import annotations
-import atexit
import dataclasses
import dis
import functools
@@ -215,10 +214,6 @@ def __init__(self) -> None:
self.stopped = False
self._activity = False
- self.in_atexit = False
- # On exit, self.in_atexit = True
- atexit.register(setattr, self, "in_atexit", True)
-
def __repr__(self) -> str:
points = sum(len(v) for v in self.data.values())
files = len(self.data)
From 39381809afa625009d214f8b3cb207ee6fb9abd0 Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Tue, 2 Jan 2024 18:57:56 -0500
Subject: [PATCH 08/41] test(fix): don't warn about changed tracers during
metacov
---
coverage/pytracer.py | 16 ++++++++++------
tests/test_oddball.py | 2 +-
tests/test_process.py | 1 +
3 files changed, 12 insertions(+), 7 deletions(-)
diff --git a/coverage/pytracer.py b/coverage/pytracer.py
index de647c0cb..9bdb9c331 100644
--- a/coverage/pytracer.py
+++ b/coverage/pytracer.py
@@ -317,12 +317,16 @@ def stop(self) -> None:
#self.log("~", "stopping on different threads")
return
- if self.warn:
- # PyPy clears the trace function before running atexit functions,
- # so don't warn if we are in atexit on PyPy and the trace function
- # has changed to None.
- dont_warn = (env.PYPY and self.in_atexit and tf is None)
- if (not dont_warn) and tf != self._cached_bound_method_trace: # pylint: disable=comparison-with-callable
+ # PyPy clears the trace function before running atexit functions,
+ # so don't warn if we are in atexit on PyPy and the trace function
+ # has changed to None. Metacoverage also messes this up, so don't
+ # warn if we are measuring ourselves.
+ suppress_warning = (
+ (env.PYPY and self.in_atexit and tf is None)
+ or env.METACOV
+ )
+ if self.warn and not suppress_warning:
+ if tf != self._cached_bound_method_trace: # pylint: disable=comparison-with-callable
self.warn(
"Trace function changed, data is likely wrong: " +
f"{tf!r} != {self._cached_bound_method_trace!r}",
diff --git a/tests/test_oddball.py b/tests/test_oddball.py
index d1decd41c..f12176ec8 100644
--- a/tests/test_oddball.py
+++ b/tests/test_oddball.py
@@ -141,7 +141,7 @@ def recur(n):
assert expected_missing == missing
# Get a warning about the stackoverflow effect on the tracing function.
- if pytrace: # pragma: no metacov
+ if pytrace and not env.METACOV: # pragma: no metacov
assert len(cov._warnings) == 1
assert re.fullmatch(
r"Trace function changed, data is likely wrong: None != " +
diff --git a/tests/test_process.py b/tests/test_process.py
index 2254757b3..98dbe701f 100644
--- a/tests/test_process.py
+++ b/tests/test_process.py
@@ -481,6 +481,7 @@ def run(self):
assert "Hello\n" in out
assert "warning" not in out
+ @pytest.mark.skipif(env.METACOV, reason="Can't test tracers changing during metacoverage")
def test_warning_trace_function_changed(self) -> None:
self.make_file("settrace.py", """\
import sys
From 1f0e3da59265e6f2006f110cc1be826458a56ebc Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Wed, 27 Dec 2023 18:30:43 -0500
Subject: [PATCH 09/41] fix: metacov under sysmon
Be explicit that we are measuring ourselves, so that we can set a
different sys.monitoring tool_id. We also defend against stopping
sys.monitoring more than once (in forking situations).
---
coverage/collector.py | 16 +++++++++++-----
coverage/control.py | 2 ++
coverage/sysmon.py | 8 ++++++--
igor.py | 1 +
tests/coveragetest.py | 23 +++++++++++++++++++++--
tests/test_concurrency.py | 3 ---
6 files changed, 41 insertions(+), 12 deletions(-)
diff --git a/coverage/collector.py b/coverage/collector.py
index dd952dcaf..1fd06dbb5 100644
--- a/coverage/collector.py
+++ b/coverage/collector.py
@@ -50,6 +50,7 @@
T = TypeVar("T")
+
class Collector:
"""Collects trace data.
@@ -84,6 +85,7 @@ def __init__(
branch: bool,
warn: TWarnFn,
concurrency: List[str],
+ metacov: bool,
) -> None:
"""Create a collector.
@@ -159,18 +161,21 @@ def __init__(
if core == "sysmon":
self._trace_class = SysMonitor
+ self._core_kwargs = {"tool_id": 3 if metacov else 1}
self.file_disposition_class = FileDisposition
self.supports_plugins = False
self.packed_arcs = False
self.systrace = False
elif core == "ctrace":
self._trace_class = CTracer
+ self._core_kwargs = {}
self.file_disposition_class = CFileDisposition
self.supports_plugins = True
self.packed_arcs = True
self.systrace = True
elif core == "pytrace":
self._trace_class = PyTracer
+ self._core_kwargs = {}
self.file_disposition_class = FileDisposition
self.supports_plugins = False
self.packed_arcs = False
@@ -297,7 +302,7 @@ def reset(self) -> None:
def _start_tracer(self) -> TTraceFn | None:
"""Start a new Tracer object, and store it in self.tracers."""
- tracer = self._trace_class()
+ tracer = self._trace_class(**self._core_kwargs)
tracer.data = self.data
tracer.trace_arcs = self.branch
tracer.should_trace = self.should_trace
@@ -414,10 +419,11 @@ def resume(self) -> None:
"""Resume tracing after a `pause`."""
for tracer in self.tracers:
tracer.start()
- if self.threading:
- self.threading.settrace(self._installation_trace)
- else:
- self._start_tracer()
+ if self.systrace:
+ if self.threading:
+ self.threading.settrace(self._installation_trace)
+ else:
+ self._start_tracer()
def post_fork(self) -> None:
"""After a fork, tracers might need to adjust."""
diff --git a/coverage/control.py b/coverage/control.py
index 0e0e01fbf..d33ef769a 100644
--- a/coverage/control.py
+++ b/coverage/control.py
@@ -262,6 +262,7 @@ def __init__( # pylint: disable=too-many-arguments
self._plugins: Plugins = Plugins()
self._data: Optional[CoverageData] = None
self._collector: Optional[Collector] = None
+ self._metacov = False
self._file_mapper: Callable[[str], str] = abs_file
self._data_suffix = self._run_suffix = None
@@ -539,6 +540,7 @@ def _init_for_start(self) -> None:
branch=self.config.branch,
warn=self._warn,
concurrency=concurrency,
+ metacov=self._metacov,
)
suffix = self._data_suffix_specified
diff --git a/coverage/sysmon.py b/coverage/sysmon.py
index 0d42ac483..0503b33d8 100644
--- a/coverage/sysmon.py
+++ b/coverage/sysmon.py
@@ -182,7 +182,7 @@ class SysMonitor(TracerCore):
# One of these will be used across threads. Be careful.
- def __init__(self) -> None:
+ def __init__(self, tool_id: int) -> None:
# Attributes set from the collector:
self.data: TTraceData
self.trace_arcs = False
@@ -195,7 +195,7 @@ def __init__(self) -> None:
# TODO: warn is unused.
self.warn: TWarnFn
- self.myid = sys.monitoring.COVERAGE_ID
+ self.myid = tool_id
# Map id(code_object) -> CodeInfo
self.code_infos: Dict[int, CodeInfo] = {}
@@ -248,6 +248,10 @@ def start(self) -> None:
@panopticon()
def stop(self) -> None:
"""Stop this Tracer."""
+ if not self.sysmon_on:
+ # In forking situations, we might try to stop when we are not
+ # started. Do nothing in that case.
+ return
assert sys_monitoring is not None
sys_monitoring.set_events(self.myid, 0)
for code in self.local_event_codes.values():
diff --git a/igor.py b/igor.py
index a0da71be8..5aa185f42 100644
--- a/igor.py
+++ b/igor.py
@@ -200,6 +200,7 @@ def run_tests_with_coverage(core, *runner_args):
cov = coverage.Coverage(config_file="metacov.ini")
cov._warn_unimported_source = False
cov._warn_preimported_source = False
+ cov._metacov = True
cov.start()
try:
diff --git a/tests/coveragetest.py b/tests/coveragetest.py
index eb2677494..c93dcb8dd 100644
--- a/tests/coveragetest.py
+++ b/tests/coveragetest.py
@@ -87,9 +87,28 @@ def start_import_stop(
The imported module is returned.
"""
- with cov.collect():
+ # Here's something I don't understand. I tried changing the code to use
+ # the handy context manager, like this:
+ #
+ # with cov.collect():
+ # # Import the Python file, executing it.
+ # return import_local_file(modname, modfile)
+ #
+ # That seemed to work, until 7.4.0 when it made metacov fail after
+ # running all the tests. The deep recursion tests in test_oddball.py
+ # seemed to cause something to be off so that a "Trace function
+ # changed" error would happen as pytest was cleaning up, failing the
+ # metacov runs. Putting back the old code below fixes it, but I don't
+ # understand the difference.
+
+ cov.start()
+ try: # pragma: nested
# Import the Python file, executing it.
- return import_local_file(modname, modfile)
+ mod = import_local_file(modname, modfile)
+ finally: # pragma: nested
+ # Stop coverage.py.
+ cov.stop()
+ return mod
def get_report(self, cov: Coverage, squeeze: bool = True, **kwargs: Any) -> str:
"""Get the report from `cov`, and canonicalize it."""
diff --git a/tests/test_concurrency.py b/tests/test_concurrency.py
index a9a818da9..db2856538 100644
--- a/tests/test_concurrency.py
+++ b/tests/test_concurrency.py
@@ -500,7 +500,6 @@ def try_multiprocessing_code(
last_line = self.squeezed_lines(out)[-1]
assert re.search(r"TOTAL \d+ 0 100%", last_line)
- @pytest.mark.skipif(env.METACOV and testenv.SYS_MON, reason="buh")
def test_multiprocessing_simple(self, start_method: str) -> None:
nprocs = 3
upto = 30
@@ -515,7 +514,6 @@ def test_multiprocessing_simple(self, start_method: str) -> None:
start_method=start_method,
)
- @pytest.mark.skipif(env.METACOV and testenv.SYS_MON, reason="buh")
def test_multiprocessing_append(self, start_method: str) -> None:
nprocs = 3
upto = 30
@@ -548,7 +546,6 @@ def test_multiprocessing_and_gevent(self, start_method: str) -> None:
start_method=start_method,
)
- @pytest.mark.skipif(env.METACOV and testenv.SYS_MON, reason="buh")
def test_multiprocessing_with_branching(self, start_method: str) -> None:
nprocs = 3
upto = 30
From cc8b892c3eee4c45bccf454d7b519205dae88c9c Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Wed, 3 Jan 2024 09:33:46 -0500
Subject: [PATCH 10/41] test: metacov is now ok with sysmon
---
.github/workflows/coverage.yml | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml
index abf4a57d6..1ca3468ad 100644
--- a/.github/workflows/coverage.yml
+++ b/.github/workflows/coverage.yml
@@ -8,9 +8,7 @@ on:
# on pull requests yet.
push:
branches:
- # sys.monitoring somehow broke metacoverage, so turn it off while we fix
- # it and get a sys.monitoring out the door.
- #- master
+ - master
- "**/*metacov*"
workflow_dispatch:
From ed6454f5267232af51250b4eadb82565d7e2c4e4 Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Thu, 4 Jan 2024 10:27:06 -0500
Subject: [PATCH 11/41] test: tools for subsetting the test suite
---
lab/pick.py | 62 ++++++++++++++++++++++++++++++++++++++++++
pyproject.toml | 2 +-
tests/conftest.py | 5 +++-
tests/select_plugin.py | 34 +++++++++++++++++++++++
4 files changed, 101 insertions(+), 2 deletions(-)
create mode 100644 lab/pick.py
create mode 100644 tests/select_plugin.py
diff --git a/lab/pick.py b/lab/pick.py
new file mode 100644
index 000000000..06274b4b4
--- /dev/null
+++ b/lab/pick.py
@@ -0,0 +1,62 @@
+# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
+# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
+
+"""
+Pick lines from a file. Blank or commented lines are ignored.
+
+Used to subset lists of tests to run. Use with the --select-cmd pytest plugin
+option.
+
+Get a list of test nodes::
+
+ .tox/py311/bin/pytest --collect-only | grep :: > tests.txt
+
+Use like this::
+
+ pytest --select-cmd="python lab/pick.py sample 10 < tests.txt"
+
+as in::
+
+ te py311 -- -vvv -n 0 --select-cmd="python lab/pick.py sample 10 < tests.txt"
+
+or::
+
+ for n in 1 1 2 2 3 3; do te py311 -- -vvv -n 0 --select-cmd="python lab/pick.py sample 3 $n < tests.txt"; done
+
+or::
+
+ for n in $(seq 1 10); do echo seed=$n; COVERAGE_COVERAGE=yes te py311 -- -n 0 --select-cmd="python lab/pick.py sample 20 $n < tests.txt"; done
+
+"""
+
+import random
+import sys
+
+args = sys.argv[1:][::-1]
+next_arg = args.pop
+
+lines = []
+for line in sys.stdin:
+ line = line.strip()
+ if not line:
+ continue
+ if line.startswith("#"):
+ continue
+ lines.append(line)
+
+mode = next_arg()
+if mode == "head":
+ number = int(next_arg())
+ lines = lines[:number]
+elif mode == "sample":
+ number = int(next_arg())
+ if args:
+ random.seed(next_arg())
+ lines = random.sample(lines, number)
+elif mode == "all":
+ pass
+else:
+ raise ValueError(f"Don't know {mode=}")
+
+for line in lines:
+ print(line)
diff --git a/pyproject.toml b/pyproject.toml
index 84fbac205..5b0719b35 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -26,7 +26,7 @@ warn_unused_configs = true
warn_unused_ignores = true
exclude = """(?x)(
- ^tests/balance_xdist_plugin\\.py$ # not part of our test suite.
+ ^tests/.*_plugin\\.py$ # not part of our test suite.
)"""
## PYLINT
diff --git a/tests/conftest.py b/tests/conftest.py
index 77d5c96f6..71f29b49a 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -30,7 +30,10 @@
# Pytest can take additional options:
# $set_env.py: PYTEST_ADDOPTS - Extra arguments to pytest.
-pytest_plugins = "tests.balance_xdist_plugin"
+pytest_plugins = [
+ "tests.balance_xdist_plugin",
+ "tests.select_plugin",
+]
@pytest.fixture(autouse=True)
diff --git a/tests/select_plugin.py b/tests/select_plugin.py
new file mode 100644
index 000000000..c3e0ea796
--- /dev/null
+++ b/tests/select_plugin.py
@@ -0,0 +1,34 @@
+# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
+# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
+
+"""
+A pytest plugin to select tests by running an external command.
+"""
+
+import subprocess
+
+
+def pytest_addoption(parser):
+ """Add command-line options for controlling the plugin."""
+ parser.addoption(
+ "--select-cmd",
+ metavar="CMD",
+ action="store",
+ default="",
+ type=str,
+ help="Command to run to get test names",
+ )
+
+
+def pytest_collection_modifyitems(
+ session, config, items
+): # pylint: disable=unused-argument
+ """Run an external command to get a list of tests to run."""
+ select_cmd = config.getoption("--select-cmd")
+ if select_cmd:
+ output = subprocess.check_output(select_cmd, shell="True").decode("utf-8")
+ test_nodeids = {
+ nodeid: seq for seq, nodeid in enumerate(output.splitlines())
+ }
+ new_items = [item for item in items if item.nodeid in test_nodeids]
+ items[:] = sorted(new_items, key=lambda item: test_nodeids[item.nodeid])
From 1ac145b2fedcb54526d62b256efde4b06c291e92 Mon Sep 17 00:00:00 2001
From: Hugo van Kemenade
Date: Fri, 5 Jan 2024 09:18:37 +0200
Subject: [PATCH 12/41] CI: Only run dependency review for upstream
---
.github/workflows/dependency-review.yml | 1 +
1 file changed, 1 insertion(+)
diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml
index 3e81f2806..248927a6c 100644
--- a/.github/workflows/dependency-review.yml
+++ b/.github/workflows/dependency-review.yml
@@ -19,6 +19,7 @@ permissions:
jobs:
dependency-review:
+ if: github.repository_owner == 'nedbat'
runs-on: ubuntu-latest
steps:
- name: 'Checkout Repository'
From 5124586e92da3e69429002b2266ce41898b953a1 Mon Sep 17 00:00:00 2001
From: Hugo van Kemenade
Date: Fri, 5 Jan 2024 09:09:15 +0200
Subject: [PATCH 13/41] Remove redundant code for Python 3.7
---
coverage/env.py | 9 ++++-----
lab/show_pyc.py | 22 ++++++++++------------
tests/test_process.py | 4 ++--
3 files changed, 16 insertions(+), 19 deletions(-)
diff --git a/coverage/env.py b/coverage/env.py
index 21fe7f041..dd0146039 100644
--- a/coverage/env.py
+++ b/coverage/env.py
@@ -60,13 +60,12 @@ class PYBEHAVIOR:
optimize_if_not_debug = 2
# 3.7 changed how functions with only docstrings are numbered.
- docstring_only_function = (not PYPY) and ((3, 7, 0, "beta", 5) <= PYVERSION <= (3, 10))
+ docstring_only_function = (not PYPY) and (PYVERSION <= (3, 10))
# When a break/continue/return statement in a try block jumps to a finally
- # block, does the finally block do the break/continue/return (pre-3.8), or
- # does the finally jump back to the break/continue/return (3.8) to do the
- # work?
- finally_jumps_back = ((3, 8) <= PYVERSION < (3, 10))
+ # block, does the finally jump back to the break/continue/return (pre-3.10)
+ # to do the work?
+ finally_jumps_back = (PYVERSION < (3, 10))
# CPython 3.11 now jumps to the decorator line again while executing
# the decorator.
diff --git a/lab/show_pyc.py b/lab/show_pyc.py
index a4f8143d4..c49cada3c 100644
--- a/lab/show_pyc.py
+++ b/lab/show_pyc.py
@@ -23,17 +23,15 @@ def show_pyc_file(fname):
magic = f.read(4)
print("magic %s" % (binascii.hexlify(magic)))
read_date_and_size = True
- if sys.version_info >= (3, 7):
- # 3.7 added a flags word
- flags = struct.unpack('= (3, 6) and line_incr >= 0x80:
+ if line_incr >= 0x80:
line_incr -= 0x100
line_num += line_incr
if line_num != last_line_num:
diff --git a/tests/test_process.py b/tests/test_process.py
index 98dbe701f..5aeb49744 100644
--- a/tests/test_process.py
+++ b/tests/test_process.py
@@ -986,8 +986,8 @@ def test_major_version_works(self) -> None:
def test_wrong_alias_doesnt_work(self) -> None:
# "coverage2" doesn't work on py3
- assert sys.version_info[0] in [2, 3] # Let us know when Python 4 is out...
- badcmd = "coverage%d" % (5 - sys.version_info[0])
+ assert sys.version_info[0] == 3 # Let us know when Python 4 is out...
+ badcmd = "coverage2"
out = self.run_command(badcmd)
assert "Code coverage for Python" not in out
From 8ddfcdf1a66d92e1aff9f8f2f13f9a57655e52f3 Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Thu, 4 Jan 2024 19:06:01 -0500
Subject: [PATCH 14/41] test: more benchmarks
---
lab/benchmark/benchmark.py | 8 +++++++-
lab/benchmark/run.py | 29 ++++++++++++++++++++++++++++-
2 files changed, 35 insertions(+), 2 deletions(-)
diff --git a/lab/benchmark/benchmark.py b/lab/benchmark/benchmark.py
index 89e5fdf28..d0d6188fd 100644
--- a/lab/benchmark/benchmark.py
+++ b/lab/benchmark/benchmark.py
@@ -341,6 +341,12 @@ def run_with_coverage(self, env, pip_args, cov_tweaks):
return duration
+class ProjectMashumaroBranch(ProjectMashumaro):
+ def __init__(self, more_pytest_args=""):
+ super().__init__(more_pytest_args="--cov-branch " + more_pytest_args)
+ self.slug = "mashbranch"
+
+
class ProjectOperator(ProjectToTest):
git_url = "https://github.com/nedbat/operator"
@@ -629,7 +635,7 @@ def run(self, num_runs: int = 3) -> None:
data = run_data[result_key]
med = statistics.median(data)
self.result_data[result_key] = med
- stdev = statistics.stdev(data)
+ stdev = statistics.stdev(data) if len(data) > 1 else 0.0
summary = (
f"Median for {proj.slug}, {pyver.slug}, {cov_ver.slug}: "
+ f"{med:.3f}s, "
diff --git a/lab/benchmark/run.py b/lab/benchmark/run.py
index b93dc874d..25d74a59c 100644
--- a/lab/benchmark/run.py
+++ b/lab/benchmark/run.py
@@ -76,7 +76,7 @@
],
)
-if 1:
+if 0:
# Compare 3.12 coverage vs no coverage
run_experiment(
py_versions=[
@@ -102,3 +102,30 @@
(f"sysmon%", "sysmon", "nocov"),
],
)
+
+if 1:
+ # Compare 3.12 coverage vs no coverage
+ run_experiment(
+ py_versions=[
+ Python(3, 12),
+ ],
+ cov_versions=[
+ NoCoverage("nocov"),
+ Coverage("732", "coverage==7.3.2"),
+ CoverageSource(
+ slug="sysmon",
+ directory="/Users/nbatchelder/coverage/trunk",
+ env_vars={"COVERAGE_CORE": "sysmon"},
+ ),
+ ],
+ projects=[
+ ProjectMashumaro(), # small: "-k ck"
+ ProjectMashumaroBranch(), # small: "-k ck"
+ ],
+ rows=["pyver", "proj"],
+ column="cov",
+ ratios=[
+ (f"732%", "732", "nocov"),
+ (f"sysmon%", "sysmon", "nocov"),
+ ],
+ )
From 2d9d6d1ca6825ff5c729c85cfa68172ec2cbac02 Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Fri, 5 Jan 2024 06:00:46 -0500
Subject: [PATCH 15/41] debug: a tool for seeing what sys.monitoring does
---
coverage/parser.py | 1 +
coverage/sysmon.py | 2 +-
lab/run_sysmon.py | 88 ++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 90 insertions(+), 1 deletion(-)
create mode 100644 lab/run_sysmon.py
diff --git a/coverage/parser.py b/coverage/parser.py
index 2fde3f7f2..9349c9ea8 100644
--- a/coverage/parser.py
+++ b/coverage/parser.py
@@ -422,6 +422,7 @@ def _line_numbers(self) -> Iterable[TLineNo]:
line numbers. Produces a sequence: l0, l1, ...
"""
if hasattr(self.code, "co_lines"):
+ # PYVERSIONS: new in 3.10
for _, _, line in self.code.co_lines():
if line:
yield line
diff --git a/coverage/sysmon.py b/coverage/sysmon.py
index 0503b33d8..793c0c248 100644
--- a/coverage/sysmon.py
+++ b/coverage/sysmon.py
@@ -227,7 +227,7 @@ def start(self) -> None:
assert sys_monitoring is not None
sys_monitoring.use_tool_id(self.myid, "coverage.py")
register = functools.partial(sys_monitoring.register_callback, self.myid)
- events = sys.monitoring.events
+ events = sys_monitoring.events
if self.trace_arcs:
sys_monitoring.set_events(
self.myid,
diff --git a/lab/run_sysmon.py b/lab/run_sysmon.py
new file mode 100644
index 000000000..a52453cc1
--- /dev/null
+++ b/lab/run_sysmon.py
@@ -0,0 +1,88 @@
+# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
+# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
+
+"""Run sys.monitoring on a file of Python code."""
+
+import functools
+import sys
+
+print(sys.version)
+the_program = sys.argv[1]
+
+code = open(the_program).read()
+
+my_id = sys.monitoring.COVERAGE_ID
+sys.monitoring.use_tool_id(my_id, "run_sysmon.py")
+register = functools.partial(sys.monitoring.register_callback, my_id)
+events = sys.monitoring.events
+
+
+def bytes_to_lines(code):
+ """Make a dict mapping byte code offsets to line numbers."""
+ b2l = {}
+ cur_line = 0
+ for bstart, bend, lineno in code.co_lines():
+ for boffset in range(bstart, bend, 2):
+ b2l[boffset] = lineno
+ return b2l
+
+
+def sysmon_py_start(code, instruction_offset):
+ print(f"PY_START: {code.co_filename}@{instruction_offset}")
+ sys.monitoring.set_local_events(
+ my_id,
+ code,
+ events.PY_RETURN | events.PY_RESUME | events.LINE | events.BRANCH | events.JUMP,
+ )
+
+
+def sysmon_py_resume(code, instruction_offset):
+ b2l = bytes_to_lines(code)
+ print(
+ f"PY_RESUME: {code.co_filename}@{instruction_offset}, "
+ + f"{b2l[instruction_offset]}"
+ )
+
+
+def sysmon_py_return(code, instruction_offset, retval):
+ b2l = bytes_to_lines(code)
+ print(
+ f"PY_RETURN: {code.co_filename}@{instruction_offset}, "
+ + f"{b2l[instruction_offset]}"
+ )
+
+
+def sysmon_line(code, line_number):
+ print(f"LINE: {code.co_filename}@{line_number}")
+ return sys.monitoring.DISABLE
+
+
+def sysmon_branch(code, instruction_offset, destination_offset):
+ b2l = bytes_to_lines(code)
+ print(
+ f"BRANCH: {code.co_filename}@{instruction_offset}->{destination_offset}, "
+ + f"{b2l[instruction_offset]}->{b2l[destination_offset]}"
+ )
+
+
+def sysmon_jump(code, instruction_offset, destination_offset):
+ b2l = bytes_to_lines(code)
+ print(
+ f"JUMP: {code.co_filename}@{instruction_offset}->{destination_offset}, "
+ + f"{b2l[instruction_offset]}->{b2l[destination_offset]}"
+ )
+
+
+sys.monitoring.set_events(
+ my_id,
+ events.PY_START | events.PY_UNWIND,
+)
+register(events.PY_START, sysmon_py_start)
+register(events.PY_RESUME, sysmon_py_resume)
+register(events.PY_RETURN, sysmon_py_return)
+# register(events.PY_UNWIND, sysmon_py_unwind_arcs)
+register(events.LINE, sysmon_line)
+register(events.BRANCH, sysmon_branch)
+register(events.JUMP, sysmon_jump)
+
+exec(code)
From f9e202a58035242b5f5efeab785f53c7ac2bfdfd Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Sat, 6 Jan 2024 06:35:19 -0500
Subject: [PATCH 16/41] debug: misc improvements to debugging
---
Makefile | 7 +++++--
coverage/config.py | 2 +-
coverage/debug.py | 1 +
lab/pick.py | 6 +++---
4 files changed, 10 insertions(+), 6 deletions(-)
diff --git a/Makefile b/Makefile
index cca356b13..0a88052d4 100644
--- a/Makefile
+++ b/Makefile
@@ -15,7 +15,7 @@ help: ## Show this help.
@echo Available targets:
@awk -F ':.*##' '/^[^: ]+:.*##/{printf " \033[1m%-20s\033[m %s\n",$$1,$$2} /^##@/{printf "\n%s\n",substr($$0,5)}' $(MAKEFILE_LIST)
-clean_platform:
+_clean_platform:
@rm -f *.so */*.so
@rm -f *.pyd */*.pyd
@rm -rf __pycache__ */__pycache__ */*/__pycache__ */*/*/__pycache__ */*/*/*/__pycache__ */*/*/*/*/__pycache__
@@ -23,7 +23,10 @@ clean_platform:
@rm -f *.pyo */*.pyo */*/*.pyo */*/*/*.pyo */*/*/*/*.pyo */*/*/*/*/*.pyo
@rm -f *$$py.class */*$$py.class */*/*$$py.class */*/*/*$$py.class */*/*/*/*$$py.class */*/*/*/*/*$$py.class
-clean: clean_platform ## Remove artifacts of test execution, installation, etc.
+debug_clean: ## Delete various debugging artifacts.
+ @rm -rf /tmp/dis $$COVERAGE_DEBUG_FILE
+
+clean: debug_clean _clean_platform ## Remove artifacts of test execution, installation, etc.
@echo "Cleaning..."
@-pip uninstall -yq coverage
@mkdir -p build # so the chmod won't fail if build doesn't exist
diff --git a/coverage/config.py b/coverage/config.py
index 2ea48b72c..24d5642b2 100644
--- a/coverage/config.py
+++ b/coverage/config.py
@@ -600,11 +600,11 @@ def read_coverage_config(
if specified_file:
raise ConfigError(f"Couldn't read {fname!r} as a config file")
- # $set_env.py: COVERAGE_DEBUG - Options for --debug.
# 3) from environment variables:
env_data_file = os.getenv("COVERAGE_FILE")
if env_data_file:
config.data_file = env_data_file
+ # $set_env.py: COVERAGE_DEBUG - Debug options: https://coverage.rtfd.io/cmd.html#debug
debugs = os.getenv("COVERAGE_DEBUG")
if debugs:
config.debug.extend(d.strip() for d in debugs.split(","))
diff --git a/coverage/debug.py b/coverage/debug.py
index 000a2cab9..8aaecb589 100644
--- a/coverage/debug.py
+++ b/coverage/debug.py
@@ -446,6 +446,7 @@ def get_one(
if file_name is not None:
fileobj = open(file_name, "a", encoding="utf-8")
else:
+ # $set_env.py: COVERAGE_DEBUG_FILE - Where to write debug output
file_name = os.getenv("COVERAGE_DEBUG_FILE", FORCED_DEBUG_FILE)
if file_name in ("stdout", "stderr"):
fileobj = getattr(sys, file_name)
diff --git a/lab/pick.py b/lab/pick.py
index 06274b4b4..ade24676c 100644
--- a/lab/pick.py
+++ b/lab/pick.py
@@ -17,15 +17,15 @@
as in::
- te py311 -- -vvv -n 0 --select-cmd="python lab/pick.py sample 10 < tests.txt"
+ te py311 -- -vvv -n 0 --cache-clear --select-cmd="python lab/pick.py sample 10 < tests.txt"
or::
- for n in 1 1 2 2 3 3; do te py311 -- -vvv -n 0 --select-cmd="python lab/pick.py sample 3 $n < tests.txt"; done
+ for n in 1 1 2 2 3 3; do te py311 -- -vvv -n 0 --cache-clear --select-cmd="python lab/pick.py sample 3 $n < tests.txt"; done
or::
- for n in $(seq 1 10); do echo seed=$n; COVERAGE_COVERAGE=yes te py311 -- -n 0 --select-cmd="python lab/pick.py sample 20 $n < tests.txt"; done
+ for n in $(seq 1 10); do echo seed=$n; COVERAGE_COVERAGE=yes te py311 -- -n 0 --cache-clear --select-cmd="python lab/pick.py sample 20 $n < tests.txt"; done
"""
From 159f67f6c6d993d7ffa831e0c856ac9306828ed7 Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Thu, 11 Jan 2024 14:50:30 -0500
Subject: [PATCH 17/41] fix: adapt to a 3.13 change in frame.f_lasti
During a yield from a generator, frame.f_lasti used to point to the
YIELD bytecode. In 3.13, it now points to the RESUME that follows it.
https://github.com/python/cpython/issues/113728
---
coverage/ctracer/tracer.c | 5 ++++-
coverage/ctracer/util.h | 5 +++++
coverage/env.py | 5 +++++
coverage/pytracer.py | 10 +++++++---
4 files changed, 21 insertions(+), 4 deletions(-)
diff --git a/coverage/ctracer/tracer.c b/coverage/ctracer/tracer.c
index 8a9e0a5ed..a05e0f8ef 100644
--- a/coverage/ctracer/tracer.c
+++ b/coverage/ctracer/tracer.c
@@ -710,7 +710,10 @@ CTracer_handle_return(CTracer *self, PyFrameObject *frame)
real_return = TRUE;
}
else {
- real_return = (code_bytes[lasti + 2] != RESUME);
+#if ENV_LASTI_IS_YIELD
+ lasti += 2;
+#endif
+ real_return = (code_bytes[lasti] != RESUME);
}
#else
/* Need to distinguish between RETURN_VALUE and YIELD_VALUE. Read
diff --git a/coverage/ctracer/util.h b/coverage/ctracer/util.h
index 1e99313dc..aaa0c98e5 100644
--- a/coverage/ctracer/util.h
+++ b/coverage/ctracer/util.h
@@ -59,6 +59,11 @@
#define MyCode_FreeCode(code)
#endif
+// Where does frame.f_lasti point when yielding from a generator?
+// It used to point at the YIELD, now it points at the RESUME.
+// https://github.com/python/cpython/issues/113728
+#define ENV_LASTI_IS_YIELD (PY_VERSION_HEX < 0x030D0000)
+
/* The values returned to indicate ok or error. */
#define RET_OK 0
#define RET_ERROR -1
diff --git a/coverage/env.py b/coverage/env.py
index dd0146039..b6b9caca3 100644
--- a/coverage/env.py
+++ b/coverage/env.py
@@ -117,6 +117,11 @@ class PYBEHAVIOR:
# PEP669 Low Impact Monitoring: https://peps.python.org/pep-0669/
pep669 = bool(getattr(sys, "monitoring", None))
+ # Where does frame.f_lasti point when yielding from a generator?
+ # It used to point at the YIELD, now it points at the RESUME.
+ # https://github.com/python/cpython/issues/113728
+ lasti_is_yield = (PYVERSION < (3, 13))
+
# Coverage.py specifics, about testing scenarios. See tests/testenv.py also.
diff --git a/coverage/pytracer.py b/coverage/pytracer.py
index 9bdb9c331..f527a4040 100644
--- a/coverage/pytracer.py
+++ b/coverage/pytracer.py
@@ -21,6 +21,7 @@
)
# We need the YIELD_VALUE opcode below, in a comparison-friendly form.
+# PYVERSIONS: RESUME is new in Python3.11
RESUME = dis.opmap.get("RESUME")
RETURN_VALUE = dis.opmap["RETURN_VALUE"]
if RESUME is None:
@@ -137,7 +138,8 @@ def _trace(
if THIS_FILE in frame.f_code.co_filename:
return None
- #self.log(":", frame.f_code.co_filename, frame.f_lineno, frame.f_code.co_name + "()", event)
+ # f = frame; code = f.f_code
+ # self.log(":", f"{code.co_filename} {f.f_lineno} {code.co_name}()", event)
if (self.stopped and sys.gettrace() == self._cached_bound_method_trace): # pylint: disable=comparison-with-callable
# The PyTrace.stop() method has been called, possibly by another
@@ -255,8 +257,10 @@ def _trace(
# A return from the end of a code object is a real return.
real_return = True
else:
- # it's a real return.
- real_return = (code[lasti + 2] != RESUME)
+ # It is a real return if we aren't going to resume next.
+ if env.PYBEHAVIOR.lasti_is_yield:
+ lasti += 2
+ real_return = (code[lasti] != RESUME)
else:
if code[lasti] == RETURN_VALUE:
real_return = True
From 58586e51fbd8fdcb6794c166cdb53d4d02684e87 Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Fri, 12 Jan 2024 09:15:44 -0500
Subject: [PATCH 18/41] fix: sysmon should ignore GeneratorExit
Also, don't use dis in production, it's changed often.
---
coverage/sysmon.py | 23 ++++++++++++-----------
tests/test_arcs.py | 3 ---
tests/test_coverage.py | 2 --
3 files changed, 12 insertions(+), 16 deletions(-)
diff --git a/coverage/sysmon.py b/coverage/sysmon.py
index 793c0c248..ab231fde1 100644
--- a/coverage/sysmon.py
+++ b/coverage/sysmon.py
@@ -6,7 +6,6 @@
from __future__ import annotations
import dataclasses
-import dis
import functools
import inspect
import os
@@ -168,12 +167,10 @@ class CodeInfo:
def bytes_to_lines(code: CodeType) -> Dict[int, int]:
"""Make a dict mapping byte code offsets to line numbers."""
b2l = {}
- cur_line = 0
- for inst in dis.get_instructions(code):
- if inst.starts_line is not None:
- cur_line = inst.starts_line
- b2l[inst.offset] = cur_line
- log(f" --> bytes_to_lines: {b2l!r}")
+ for bstart, bend, lineno in code.co_lines():
+ if lineno is not None:
+ for boffset in range(bstart, bend, 2):
+ b2l[boffset] = lineno
return b2l
@@ -379,26 +376,30 @@ def sysmon_py_return_arcs(
last_line = self.last_lines.get(frame)
if last_line is not None:
arc = (last_line, -code.co_firstlineno)
+ # log(f"adding {arc=}")
cast(Set[TArc], code_info.file_data).add(arc)
# Leaving this function, no need for the frame any more.
self.last_lines.pop(frame, None)
- @panopticon("code", "@", None)
+ @panopticon("code", "@", "exc")
def sysmon_py_unwind_arcs(
self, code: CodeType, instruction_offset: int, exception: BaseException
) -> MonitorReturn:
"""Handle sys.monitoring.events.PY_UNWIND events for branch coverage."""
frame = self.callers_frame()
+ # Leaving this function.
+ last_line = self.last_lines.pop(frame, None)
+ if isinstance(exception, GeneratorExit):
+ # We don't want to count generator exits as arcs.
+ return
code_info = self.code_infos.get(id(code))
if code_info is not None and code_info.file_data is not None:
- last_line = self.last_lines.get(frame)
if last_line is not None:
arc = (last_line, -code.co_firstlineno)
+ # log(f"adding {arc=}")
cast(Set[TArc], code_info.file_data).add(arc)
- # Leaving this function.
- self.last_lines.pop(frame, None)
@panopticon("code", "line")
def sysmon_line_lines(self, code: CodeType, line_number: int) -> MonitorReturn:
diff --git a/tests/test_arcs.py b/tests/test_arcs.py
index 55132b896..7331eb32a 100644
--- a/tests/test_arcs.py
+++ b/tests/test_arcs.py
@@ -7,7 +7,6 @@
import pytest
-from tests import testenv
from tests.coveragetest import CoverageTest
from tests.helpers import assert_count_equal, xfail_pypy38
@@ -1308,7 +1307,6 @@ def gen(inp):
arcz=".1 19 9. .2 23 34 45 56 63 37 7.",
)
- @pytest.mark.xfail(testenv.SYS_MON, reason="TODO: fix this for sys.monitoring")
def test_abandoned_yield(self) -> None:
# https://github.com/nedbat/coveragepy/issues/440
self.check_coverage("""\
@@ -1651,7 +1649,6 @@ def test_pathologically_long_code_object(self, n: int) -> None:
self.check_coverage(code, arcs=[(-1, 1), (1, 2*n+4), (2*n+4, -1)])
assert self.stdout() == f"{n}\n"
- @pytest.mark.xfail(testenv.SYS_MON, reason="TODO: fix this for sys.monitoring")
def test_partial_generators(self) -> None:
# https://github.com/nedbat/coveragepy/issues/475
# Line 2 is executed completely.
diff --git a/tests/test_coverage.py b/tests/test_coverage.py
index e4157ecf0..96639c072 100644
--- a/tests/test_coverage.py
+++ b/tests/test_coverage.py
@@ -11,7 +11,6 @@
from coverage import env
from coverage.exceptions import NoDataError
-from tests import testenv
from tests.coveragetest import CoverageTest
@@ -1407,7 +1406,6 @@ def test_excluding_try_except_stranded_else(self) -> None:
arcz_missing=arcz_missing,
)
- @pytest.mark.xfail(testenv.SYS_MON, reason="TODO: fix this for sys.monitoring")
def test_excluded_comprehension_branches(self) -> None:
# https://github.com/nedbat/coveragepy/issues/1271
self.check_coverage("""\
From 3f71b590841f59a02797eb604536968906cdf5ee Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Sat, 13 Jan 2024 06:42:54 -0500
Subject: [PATCH 19/41] build: remove gevent pin, make upgrade
No idea what changed to require the pin once, and now it's not needed.
With the pin, installing requirements on 3.8, 3.9 would get stuck.
Without the pin, everything works. win-win.
---
requirements/dev.pip | 16 +++++++++-------
requirements/light-threads.pip | 10 +++-------
requirements/mypy.pip | 6 +++---
requirements/pins.pip | 6 ------
requirements/pytest.pip | 6 +++---
requirements/tox.pip | 2 +-
6 files changed, 19 insertions(+), 27 deletions(-)
diff --git a/requirements/dev.pip b/requirements/dev.pip
index 3cb3eeabb..755bda697 100644
--- a/requirements/dev.pip
+++ b/requirements/dev.pip
@@ -6,7 +6,7 @@
#
astroid==3.0.2
# via pylint
-attrs==23.1.0
+attrs==23.2.0
# via hypothesis
build==1.0.3
# via check-manifest
@@ -47,7 +47,7 @@ flaky==3.7.0
# via -r requirements/pytest.in
greenlet==3.0.3
# via -r requirements/dev.in
-hypothesis==6.92.1
+hypothesis==6.93.0
# via -r requirements/pytest.in
idna==3.6
# via requests
@@ -68,7 +68,7 @@ jedi==0.19.1
# via pudb
keyring==24.3.0
# via twine
-libsass==0.22.0
+libsass==0.23.0
# via -r requirements/dev.in
markdown-it-py==3.0.0
# via rich
@@ -76,7 +76,7 @@ mccabe==0.7.0
# via pylint
mdurl==0.1.2
# via markdown-it-py
-more-itertools==10.1.0
+more-itertools==10.2.0
# via jaraco-classes
nh3==0.2.15
# via readme-renderer
@@ -113,7 +113,7 @@ pyproject-api==1.6.1
# via tox
pyproject-hooks==1.0.0
# via build
-pytest==7.4.3
+pytest==7.4.4
# via
# -r requirements/pytest.in
# pytest-xdist
@@ -136,6 +136,8 @@ rich==13.7.0
# via twine
sortedcontainers==2.4.0
# via hypothesis
+tabulate==0.9.0
+ # via -r requirements/dev.in
tomli==2.0.1
# via
# build
@@ -147,7 +149,7 @@ tomli==2.0.1
# tox
tomlkit==0.12.3
# via pylint
-tox==4.11.4
+tox==4.12.0
# via
# -r requirements/tox.in
# tox-gh
@@ -164,7 +166,7 @@ urllib3==2.1.0
# via
# requests
# twine
-urwid==2.3.4
+urwid==2.4.2
# via
# pudb
# urwid-readline
diff --git a/requirements/light-threads.pip b/requirements/light-threads.pip
index 14de2f78a..67cbd19cd 100644
--- a/requirements/light-threads.pip
+++ b/requirements/light-threads.pip
@@ -8,12 +8,10 @@ cffi==1.16.0
# via -r requirements/light-threads.in
dnspython==2.4.2
# via eventlet
-eventlet==0.34.2
+eventlet==0.34.3
+ # via -r requirements/light-threads.in
+gevent==23.9.1
# via -r requirements/light-threads.in
-gevent==23.7.0
- # via
- # -c requirements/pins.pip
- # -r requirements/light-threads.in
greenlet==3.0.3
# via
# -r requirements/light-threads.in
@@ -21,8 +19,6 @@ greenlet==3.0.3
# gevent
pycparser==2.21
# via cffi
-six==1.16.0
- # via eventlet
zope-event==5.0
# via gevent
zope-interface==6.1
diff --git a/requirements/mypy.pip b/requirements/mypy.pip
index 6b2f55487..294b3503c 100644
--- a/requirements/mypy.pip
+++ b/requirements/mypy.pip
@@ -4,7 +4,7 @@
#
# make upgrade
#
-attrs==23.1.0
+attrs==23.2.0
# via hypothesis
colorama==0.4.6
# via -r requirements/pytest.in
@@ -16,7 +16,7 @@ execnet==2.0.2
# via pytest-xdist
flaky==3.7.0
# via -r requirements/pytest.in
-hypothesis==6.92.1
+hypothesis==6.93.0
# via -r requirements/pytest.in
iniconfig==2.0.0
# via pytest
@@ -28,7 +28,7 @@ packaging==23.2
# via pytest
pluggy==1.3.0
# via pytest
-pytest==7.4.3
+pytest==7.4.4
# via
# -r requirements/pytest.in
# pytest-xdist
diff --git a/requirements/pins.pip b/requirements/pins.pip
index dad91cc19..97e4b2974 100644
--- a/requirements/pins.pip
+++ b/requirements/pins.pip
@@ -2,12 +2,6 @@
# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
# Version pins, for use as a constraints file.
-# gevent depends on greenlet>=3.0rc3, which causes problems:
-# The conflict is caused by:
-# The user requested greenlet==2.0.2
-# eventlet 0.33.3 depends on greenlet>=0.3
-# gevent 23.9.1 depends on greenlet>=3.0rc3; platform_python_implementation == "CPython" and python_version >= "3.11"
-gevent<23.9
# sphinx-rtd-theme wants <7
# https://github.com/readthedocs/sphinx_rtd_theme/issues/1463
diff --git a/requirements/pytest.pip b/requirements/pytest.pip
index bac12204b..ff1c88f97 100644
--- a/requirements/pytest.pip
+++ b/requirements/pytest.pip
@@ -4,7 +4,7 @@
#
# make upgrade
#
-attrs==23.1.0
+attrs==23.2.0
# via hypothesis
colorama==0.4.6
# via -r requirements/pytest.in
@@ -16,7 +16,7 @@ execnet==2.0.2
# via pytest-xdist
flaky==3.7.0
# via -r requirements/pytest.in
-hypothesis==6.92.1
+hypothesis==6.93.0
# via -r requirements/pytest.in
iniconfig==2.0.0
# via pytest
@@ -24,7 +24,7 @@ packaging==23.2
# via pytest
pluggy==1.3.0
# via pytest
-pytest==7.4.3
+pytest==7.4.4
# via
# -r requirements/pytest.in
# pytest-xdist
diff --git a/requirements/tox.pip b/requirements/tox.pip
index 82a2ba72e..bf1ec92ac 100644
--- a/requirements/tox.pip
+++ b/requirements/tox.pip
@@ -34,7 +34,7 @@ tomli==2.0.1
# via
# pyproject-api
# tox
-tox==4.11.4
+tox==4.12.0
# via
# -r requirements/tox.in
# tox-gh
From 45e7854266dd6eff86251fd66771dd3baa24eff0 Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Tue, 3 Oct 2023 06:57:08 -0400
Subject: [PATCH 20/41] test: don't warn about unclosed SQLite connections
But add more logging that we used while determining it wasn't a real problem.
---
coverage/sqlitedb.py | 5 +++++
tests/conftest.py | 10 ++++++++++
2 files changed, 15 insertions(+)
diff --git a/coverage/sqlitedb.py b/coverage/sqlitedb.py
index 15fd1834e..468436bdd 100644
--- a/coverage/sqlitedb.py
+++ b/coverage/sqlitedb.py
@@ -53,6 +53,9 @@ def _connect(self) -> None:
except sqlite3.Error as exc:
raise DataError(f"Couldn't use data file {self.filename!r}: {exc}") from exc
+ if self.debug.should("sql"):
+ self.debug.write(f"Connected to {self.filename!r} as {self.con!r}")
+
self.con.create_function("REGEXP", 2, lambda txt, pat: re.search(txt, pat) is not None)
# Turning off journal_mode can speed up writing. It can't always be
@@ -75,6 +78,8 @@ def _connect(self) -> None:
def close(self) -> None:
"""If needed, close the connection."""
if self.con is not None and self.filename != ":memory:":
+ if self.debug.should("sql"):
+ self.debug.write(f"Closing {self.con!r} on {self.filename!r}")
self.con.close()
self.con = None
diff --git a/tests/conftest.py b/tests/conftest.py
index 71f29b49a..9917d4f93 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -66,6 +66,16 @@ def set_warnings() -> None:
# pypy3 warns about unclosed files a lot.
warnings.filterwarnings("ignore", r".*unclosed file", category=ResourceWarning)
+ # Don't warn about unclosed SQLite connections.
+ # We don't close ":memory:" databases because we don't have a way to connect
+ # to them more than once if we close them. In real coverage.py uses, there
+ # are only a couple of them, but our test suite makes many and we get warned
+ # about them all.
+ # Python3.13 added this warning, but the behavior has been the same all along,
+ # without any reported problems, so just quiet the warning.
+ # https://github.com/python/cpython/issues/105539
+ warnings.filterwarnings("ignore", r"unclosed database", category=ResourceWarning)
+
@pytest.fixture(autouse=True)
def reset_sys_path() -> Iterator[None]:
From aee524de5ca6bf630c1f2437ca3e240b1a9b7830 Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Sun, 1 Oct 2023 08:08:10 -0400
Subject: [PATCH 21/41] build: claim 3.13 support
---
.github/workflows/python-nightly.yml | 1 +
README.rst | 2 +-
doc/index.rst | 2 +-
setup.py | 1 +
tox.ini | 5 +++--
5 files changed, 7 insertions(+), 4 deletions(-)
diff --git a/.github/workflows/python-nightly.yml b/.github/workflows/python-nightly.yml
index 2f9ae2a18..c4bffd278 100644
--- a/.github/workflows/python-nightly.yml
+++ b/.github/workflows/python-nightly.yml
@@ -49,6 +49,7 @@ jobs:
# https://launchpad.net/~deadsnakes/+archive/ubuntu/nightly/+packages
- "3.11-dev"
- "3.12-dev"
+ - "3.13-dev"
# https://github.com/actions/setup-python#available-versions-of-pypy
- "pypy-3.8-nightly"
- "pypy-3.9-nightly"
diff --git a/README.rst b/README.rst
index a9038b890..8ceedbf08 100644
--- a/README.rst
+++ b/README.rst
@@ -25,7 +25,7 @@ Coverage.py runs on these versions of Python:
.. PYVERSIONS
-* CPython 3.8 through 3.12.
+* Python 3.8 through 3.12, and 3.13.0a3 and up.
* PyPy3 versions 3.8 through 3.10.
Documentation is on `Read the Docs`_. Code repository and issue tracker are on
diff --git a/doc/index.rst b/doc/index.rst
index 169f61f98..522c4a6bf 100644
--- a/doc/index.rst
+++ b/doc/index.rst
@@ -18,7 +18,7 @@ supported on:
.. PYVERSIONS
-* Python versions 3.8 through 3.12.
+* Python 3.8 through 3.12, and 3.13.0a3 and up.
* PyPy3 versions 3.8 through 3.10.
.. ifconfig:: prerelease
diff --git a/setup.py b/setup.py
index ab75d2c65..da105fcd9 100644
--- a/setup.py
+++ b/setup.py
@@ -31,6 +31,7 @@
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 :: Quality Assurance
diff --git a/tox.ini b/tox.ini
index d9540e51b..b756e175c 100644
--- a/tox.ini
+++ b/tox.ini
@@ -4,7 +4,7 @@
[tox]
# When changing this list, be sure to check the [gh] list below.
# PYVERSIONS
-envlist = py3{8,9,10,11,12}, pypy3, doc, lint, mypy
+envlist = py3{8,9,10,11,12,13}, pypy3, doc, lint, mypy
skip_missing_interpreters = {env:COVERAGE_SKIP_MISSING_INTERPRETERS:True}
toxworkdir = {env:TOXWORKDIR:.tox}
@@ -43,7 +43,7 @@ commands =
python -m pip install {env:COVERAGE_PIP_ARGS} -q -e .
python igor.py test_with_core ctrace {posargs}
- py3{12}: python igor.py test_with_core sysmon {posargs}
+ py3{12,13},anypy: python igor.py test_with_core sysmon {posargs}
# Remove the C extension so that we can test the PyTracer
python igor.py remove_extension
@@ -124,4 +124,5 @@ python =
3.10 = py310
3.11 = py311
3.12 = py312
+ 3.13 = py313
pypy-3 = pypy3
From a464345d4a538c2a449b0d2086c7b001f69ffd7d Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Sun, 14 Jan 2024 07:04:40 -0500
Subject: [PATCH 22/41] test(build): remove unneeded warning suppressions
---
pyproject.toml | 10 +++++-----
tests/conftest.py | 17 -----------------
tox.ini | 3 ++-
3 files changed, 7 insertions(+), 23 deletions(-)
diff --git a/pyproject.toml b/pyproject.toml
index 5b0719b35..b22a0d9cd 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -116,11 +116,11 @@ markers = [
# How come these warnings are suppressed successfully here, but not in conftest.py??
filterwarnings = [
- "ignore:the imp module is deprecated in favour of importlib:DeprecationWarning",
- "ignore:distutils Version classes are deprecated:DeprecationWarning",
- "ignore:The distutils package is deprecated and slated for removal in Python 3.12:DeprecationWarning",
- # Pytest warns if it can't collect things that seem to be tests. This should be an error.
- "error::pytest.PytestCollectionWarning",
+ # Sample 'ignore':
+ #"ignore:the imp module is deprecated in favour of importlib:DeprecationWarning",
+
+ ## Pytest warns if it can't collect things that seem to be tests. This should be an error.
+ #"error::pytest.PytestCollectionWarning",
]
# xfail tests that pass should fail the test suite
diff --git a/tests/conftest.py b/tests/conftest.py
index 9917d4f93..d19642030 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -45,23 +45,6 @@ def set_warnings() -> None:
# Warnings to suppress:
# How come these warnings are successfully suppressed here, but not in pyproject.toml??
- warnings.filterwarnings(
- "ignore",
- category=DeprecationWarning,
- message=r".*imp module is deprecated in favour of importlib",
- )
-
- warnings.filterwarnings(
- "ignore",
- category=DeprecationWarning,
- message=r"module 'sre_constants' is deprecated",
- )
-
- warnings.filterwarnings(
- "ignore",
- category=pytest.PytestRemovedIn8Warning,
- )
-
if env.PYPY:
# pypy3 warns about unclosed files a lot.
warnings.filterwarnings("ignore", r".*unclosed file", category=ResourceWarning)
diff --git a/tox.ini b/tox.ini
index b756e175c..746b9a785 100644
--- a/tox.ini
+++ b/tox.ini
@@ -30,7 +30,8 @@ setenv =
# For some tests, we need .pyc files written in the current directory,
# so override any local setting.
PYTHONPYCACHEPREFIX=
- PYTHONWARNINGS=ignore:removed in Python 3.14; use ast.Constant:DeprecationWarning
+ # If we ever need a stronger way to suppress warnings:
+ #PYTHONWARNINGS=ignore:removed in Python 3.14; use ast.Constant:DeprecationWarning
# $set_env.py: COVERAGE_PIP_ARGS - Extra arguments for `pip install`
# `--no-build-isolation` will let tox work with no network.
From c798297b6db347e0dbee329d46f42dbd33871cca Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Sun, 14 Jan 2024 09:28:43 -0500
Subject: [PATCH 23/41] docs: document and clean up select_plugin.py and
pick.py
---
lab/pick.py | 40 ++++++++++++++++++++++++----------------
tests/select_plugin.py | 7 ++++---
2 files changed, 28 insertions(+), 19 deletions(-)
diff --git a/lab/pick.py b/lab/pick.py
index ade24676c..f0d8b2bf7 100644
--- a/lab/pick.py
+++ b/lab/pick.py
@@ -2,30 +2,43 @@
# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
"""
-Pick lines from a file. Blank or commented lines are ignored.
+Pick lines from the standard input. Blank or commented lines are ignored.
Used to subset lists of tests to run. Use with the --select-cmd pytest plugin
option.
-Get a list of test nodes::
+The first command line argument is a mode for selection. Other arguments depend
+on the mode. Only one mode is currently implemented: sample.
- .tox/py311/bin/pytest --collect-only | grep :: > tests.txt
+Modes:
-Use like this::
+ - ``sample``: randomly sample N lines from the input.
- pytest --select-cmd="python lab/pick.py sample 10 < tests.txt"
+ - the first argument is N, the number of lines you want.
-as in::
+ - the second argument is optional: a seed for the randomizer.
+ Using the same seed will produce the same output.
- te py311 -- -vvv -n 0 --cache-clear --select-cmd="python lab/pick.py sample 10 < tests.txt"
+Examples:
-or::
+Get a list of test nodes::
+
+ pytest --collect-only | grep :: > tests.txt
- for n in 1 1 2 2 3 3; do te py311 -- -vvv -n 0 --cache-clear --select-cmd="python lab/pick.py sample 3 $n < tests.txt"; done
+Use like this::
+
+ pytest --cache-clear --select-cmd="python pick.py sample 10 < tests.txt"
+
+For coverage.py specifically::
+
+ tox -q -e py311 -- -n 0 --cache-clear --select-cmd="python lab/pick.py sample 10 < tests.txt"
or::
- for n in $(seq 1 10); do echo seed=$n; COVERAGE_COVERAGE=yes te py311 -- -n 0 --cache-clear --select-cmd="python lab/pick.py sample 20 $n < tests.txt"; done
+ for n in $(seq 1 100); do \
+ echo seed=$n; \
+ tox -q -e py311 -- -n 0 --cache-clear --select-cmd="python lab/pick.py sample 3 $n < tests.txt"; \
+ done
"""
@@ -45,16 +58,11 @@
lines.append(line)
mode = next_arg()
-if mode == "head":
- number = int(next_arg())
- lines = lines[:number]
-elif mode == "sample":
+if mode == "sample":
number = int(next_arg())
if args:
random.seed(next_arg())
lines = random.sample(lines, number)
-elif mode == "all":
- pass
else:
raise ValueError(f"Don't know {mode=}")
diff --git a/tests/select_plugin.py b/tests/select_plugin.py
index c3e0ea796..5f4828878 100644
--- a/tests/select_plugin.py
+++ b/tests/select_plugin.py
@@ -3,6 +3,9 @@
"""
A pytest plugin to select tests by running an external command.
+
+See lab/pick.py for how to use pick.py to subset test suites.
+
"""
import subprocess
@@ -20,9 +23,7 @@ def pytest_addoption(parser):
)
-def pytest_collection_modifyitems(
- session, config, items
-): # pylint: disable=unused-argument
+def pytest_collection_modifyitems(config, items):
"""Run an external command to get a list of tests to run."""
select_cmd = config.getoption("--select-cmd")
if select_cmd:
From 0bc5e4a9458867ae133197cafc93787de1ccd222 Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Sun, 14 Jan 2024 13:22:27 -0500
Subject: [PATCH 24/41] docs: link to the blog post about select_plugin.py and
pick.py
---
lab/pick.py | 2 ++
tests/select_plugin.py | 2 ++
2 files changed, 4 insertions(+)
diff --git a/lab/pick.py b/lab/pick.py
index f0d8b2bf7..1dd4530aa 100644
--- a/lab/pick.py
+++ b/lab/pick.py
@@ -40,6 +40,8 @@
tox -q -e py311 -- -n 0 --cache-clear --select-cmd="python lab/pick.py sample 3 $n < tests.txt"; \
done
+More about this: https://nedbatchelder.com/blog/202401/randomly_subsetting_test_suites.html
+
"""
import random
diff --git a/tests/select_plugin.py b/tests/select_plugin.py
index 5f4828878..9ca48ff35 100644
--- a/tests/select_plugin.py
+++ b/tests/select_plugin.py
@@ -6,6 +6,8 @@
See lab/pick.py for how to use pick.py to subset test suites.
+More about this: https://nedbatchelder.com/blog/202401/randomly_subsetting_test_suites.html
+
"""
import subprocess
From 2bbcb06055ce2d0551eb8926a1849dd4c9946392 Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Mon, 15 Jan 2024 06:03:27 -0500
Subject: [PATCH 25/41] test(build): this warning tweak should remain
---
pyproject.toml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pyproject.toml b/pyproject.toml
index b22a0d9cd..0cafdb235 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -120,7 +120,7 @@ filterwarnings = [
#"ignore:the imp module is deprecated in favour of importlib:DeprecationWarning",
## Pytest warns if it can't collect things that seem to be tests. This should be an error.
- #"error::pytest.PytestCollectionWarning",
+ "error::pytest.PytestCollectionWarning",
]
# xfail tests that pass should fail the test suite
From 576cb3eb5f11d2089ef99449469787f31aeefbab Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Mon, 15 Jan 2024 08:46:59 -0500
Subject: [PATCH 26/41] feat: JSON report now has an explicit format version
indicator #1732
---
CHANGES.rst | 5 ++++-
coverage/jsonreport.py | 5 +++++
tests/test_json.py | 10 ++++++----
3 files changed, 15 insertions(+), 5 deletions(-)
diff --git a/CHANGES.rst b/CHANGES.rst
index 59071a8ee..55384553b 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -20,7 +20,10 @@ development at the same time, such as 4.5.x and 5.0.
Unreleased
----------
-Nothing yet.
+- Fix: the JSON report now includes an explicit format version number, closing
+ `issue 1732`_.
+
+.. _issue 1732: https://github.com/nedbat/coveragepy/issues/1732
.. scriv-start-here
diff --git a/coverage/jsonreport.py b/coverage/jsonreport.py
index 9780e261a..0820c816e 100644
--- a/coverage/jsonreport.py
+++ b/coverage/jsonreport.py
@@ -21,6 +21,10 @@
from coverage.data import CoverageData
+# "Version 1" had no format number at all.
+# 2: add the meta.format field.
+FORMAT_VERSION = 2
+
class JsonReporter:
"""A reporter for writing JSON coverage results."""
@@ -44,6 +48,7 @@ def report(self, morfs: Optional[Iterable[TMorf]], outfile: IO[str]) -> float:
coverage_data = self.coverage.get_data()
coverage_data.set_query_contexts(self.config.report_contexts)
self.report_data["meta"] = {
+ "format": FORMAT_VERSION,
"version": __version__,
"timestamp": datetime.datetime.now().isoformat(),
"branch_coverage": coverage_data.has_arcs(),
diff --git a/tests/test_json.py b/tests/test_json.py
index acfdbba77..27aab867f 100644
--- a/tests/test_json.py
+++ b/tests/test_json.py
@@ -13,6 +13,7 @@
import coverage
from coverage import Coverage
+from coverage.jsonreport import FORMAT_VERSION
from tests.coveragetest import UsingModulesMixin, CoverageTest
@@ -26,7 +27,7 @@ def _assert_expected_json_report(
expected_result: Dict[str, Any],
) -> None:
"""
- Helper for tests that handles the common ceremony so the tests can be clearly show the
+ Helper that handles common ceremonies so tests can clearly show the
consequences of setting various arguments.
"""
self.make_file("a.py", """\
@@ -49,13 +50,16 @@ def _assert_expected_json_report(
datetime.strptime(parsed_result['meta']['timestamp'], "%Y-%m-%dT%H:%M:%S.%f")
)
del (parsed_result['meta']['timestamp'])
+ expected_result["meta"].update({
+ "format": FORMAT_VERSION,
+ "version": coverage.__version__,
+ })
assert parsed_result == expected_result
def test_branch_coverage(self) -> None:
cov = coverage.Coverage(branch=True)
expected_result = {
'meta': {
- "version": coverage.__version__,
"branch_coverage": True,
"show_contexts": False,
},
@@ -107,7 +111,6 @@ def test_simple_line_coverage(self) -> None:
cov = coverage.Coverage()
expected_result = {
'meta': {
- "version": coverage.__version__,
"branch_coverage": False,
"show_contexts": False,
},
@@ -152,7 +155,6 @@ def run_context_test(self, relative_files: bool) -> None:
cov = coverage.Coverage(context="cool_test", config_file="config")
expected_result = {
'meta': {
- "version": coverage.__version__,
"branch_coverage": False,
"show_contexts": True,
},
From 1a416b678695531c0131065e5dc9cc621a47ed5f Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Mon, 15 Jan 2024 09:02:36 -0500
Subject: [PATCH 27/41] docs: leave a note in status.json pointing to the right
place to look
In https://github.com/nedbat/coveragepy/issues/1730 and
https://github.com/nedbat/coveragepy/issues/1732, it's clear that people
might poke around and find apparently useful information in the
htmlcov/status.json file. Add a note to try to get them to the place
they want to be.
---
coverage/html.py | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/coverage/html.py b/coverage/html.py
index 7b827a794..5a571dac0 100644
--- a/coverage/html.py
+++ b/coverage/html.py
@@ -506,6 +506,11 @@ class IncrementalChecker:
STATUS_FILE = "status.json"
STATUS_FORMAT = 2
+ NOTE = (
+ "This file is an internal implementation detail to speed up HTML report"
+ + " generation. Its format can change at any time. You might be looking"
+ + " for the JSON report: https://coverage.rtfd.io/cmd.html#cmd-json"
+ )
# The data looks like:
#
@@ -578,6 +583,7 @@ def write(self) -> None:
files[filename] = fileinfo
status = {
+ "note": self.NOTE,
"format": self.STATUS_FORMAT,
"version": coverage.__version__,
"globals": self.globals,
From 3c5b75646d2f1639a182a0339efa7aa939aa1230 Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Sat, 20 Jan 2024 09:31:03 -0500
Subject: [PATCH 28/41] docs: correct the contributing docs
---
doc/contributing.rst | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/doc/contributing.rst b/doc/contributing.rst
index 10f7b1cc6..359d49fc1 100644
--- a/doc/contributing.rst
+++ b/doc/contributing.rst
@@ -36,6 +36,8 @@ you frustration.
Getting the code
----------------
+.. PYVERSIONS (mention of lowest version in the "create virtualenv" step).
+
The coverage.py code is hosted on a GitHub repository at
https://github.com/nedbat/coveragepy. To get a working environment, follow
these steps:
@@ -47,6 +49,7 @@ these steps:
#. (Optional) Create a virtualenv to work in, and activate it. There
are a number of ways to do this. Use the method you are comfortable with.
+ Ideally, use Python3.8 (the lowest version coverage.py supports).
#. Clone the repository::
@@ -55,7 +58,7 @@ these steps:
#. Install the requirements::
- $ python3 -m pip install -r requirements/dev.in
+ $ python3 -m pip install -r requirements/dev.pip
Note: You may need to upgrade pip to install the requirements.
From 443dd7e63b88d475f5fd3a30e5df64c67a7c5b49 Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Tue, 23 Jan 2024 06:57:55 -0500
Subject: [PATCH 29/41] chore: make upgrade
---
requirements/dev.pip | 8 ++++----
requirements/light-threads.pip | 4 ++--
requirements/mypy.pip | 2 +-
requirements/pytest.pip | 2 +-
requirements/tox.pip | 2 +-
5 files changed, 9 insertions(+), 9 deletions(-)
diff --git a/requirements/dev.pip b/requirements/dev.pip
index 755bda697..a6a17e250 100644
--- a/requirements/dev.pip
+++ b/requirements/dev.pip
@@ -47,7 +47,7 @@ flaky==3.7.0
# via -r requirements/pytest.in
greenlet==3.0.3
# via -r requirements/dev.in
-hypothesis==6.93.0
+hypothesis==6.96.4
# via -r requirements/pytest.in
idna==3.6
# via requests
@@ -100,7 +100,7 @@ pluggy==1.3.0
# via
# pytest
# tox
-pudb==2023.1
+pudb==2024.1
# via -r requirements/dev.in
pygments==2.17.2
# via
@@ -149,7 +149,7 @@ tomli==2.0.1
# tox
tomlkit==0.12.3
# via pylint
-tox==4.12.0
+tox==4.12.1
# via
# -r requirements/tox.in
# tox-gh
@@ -166,7 +166,7 @@ urllib3==2.1.0
# via
# requests
# twine
-urwid==2.4.2
+urwid==2.4.6
# via
# pudb
# urwid-readline
diff --git a/requirements/light-threads.pip b/requirements/light-threads.pip
index 67cbd19cd..495aa3192 100644
--- a/requirements/light-threads.pip
+++ b/requirements/light-threads.pip
@@ -6,9 +6,9 @@
#
cffi==1.16.0
# via -r requirements/light-threads.in
-dnspython==2.4.2
+dnspython==2.5.0
# via eventlet
-eventlet==0.34.3
+eventlet==0.35.0
# via -r requirements/light-threads.in
gevent==23.9.1
# via -r requirements/light-threads.in
diff --git a/requirements/mypy.pip b/requirements/mypy.pip
index 294b3503c..437f1c92c 100644
--- a/requirements/mypy.pip
+++ b/requirements/mypy.pip
@@ -16,7 +16,7 @@ execnet==2.0.2
# via pytest-xdist
flaky==3.7.0
# via -r requirements/pytest.in
-hypothesis==6.93.0
+hypothesis==6.96.4
# via -r requirements/pytest.in
iniconfig==2.0.0
# via pytest
diff --git a/requirements/pytest.pip b/requirements/pytest.pip
index ff1c88f97..e40278a69 100644
--- a/requirements/pytest.pip
+++ b/requirements/pytest.pip
@@ -16,7 +16,7 @@ execnet==2.0.2
# via pytest-xdist
flaky==3.7.0
# via -r requirements/pytest.in
-hypothesis==6.93.0
+hypothesis==6.96.4
# via -r requirements/pytest.in
iniconfig==2.0.0
# via pytest
diff --git a/requirements/tox.pip b/requirements/tox.pip
index bf1ec92ac..8ed94b4e3 100644
--- a/requirements/tox.pip
+++ b/requirements/tox.pip
@@ -34,7 +34,7 @@ tomli==2.0.1
# via
# pyproject-api
# tox
-tox==4.12.0
+tox==4.12.1
# via
# -r requirements/tox.in
# tox-gh
From 7716ee4b7de1d07996ed6db4f376ed6c0cdfa760 Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Tue, 23 Jan 2024 08:36:21 -0500
Subject: [PATCH 30/41] test: an obscure test is more unpredictable: skip it
---
tests/test_arcs.py | 12 ++++++++----
1 file changed, 8 insertions(+), 4 deletions(-)
diff --git a/tests/test_arcs.py b/tests/test_arcs.py
index 7331eb32a..615dd557a 100644
--- a/tests/test_arcs.py
+++ b/tests/test_arcs.py
@@ -1851,7 +1851,11 @@ def test_lambda_in_dict(self) -> None:
)
-xfail_eventlet_670 = pytest.mark.xfail(
+# This had been a failure on Mac 3.9, but it started passing on GitHub
+# actions (running macOS 12) but still failed on my laptop (macOS 14).
+# I don't understand why it failed, I don't understand why it passed,
+# so just skip the whole thing.
+skip_eventlet_670 = pytest.mark.skipif(
env.PYVERSION[:2] == (3, 9) and env.CPYTHON and env.OSX,
reason="Avoid an eventlet bug on Mac 3.9: eventlet#670",
# https://github.com/eventlet/eventlet/issues/670
@@ -1861,7 +1865,7 @@ def test_lambda_in_dict(self) -> None:
class AsyncTest(CoverageTest):
"""Tests of the new async and await keywords in Python 3.5"""
- @xfail_eventlet_670
+ @skip_eventlet_670
def test_async(self) -> None:
self.check_coverage("""\
import asyncio
@@ -1888,7 +1892,7 @@ async def print_sum(x, y): # 8
)
assert self.stdout() == "Compute 1 + 2 ...\n1 + 2 = 3\n"
- @xfail_eventlet_670
+ @skip_eventlet_670
def test_async_for(self) -> None:
self.check_coverage("""\
import asyncio
@@ -1985,7 +1989,7 @@ async def async_test():
# https://github.com/nedbat/coveragepy/issues/1176
# https://bugs.python.org/issue44622
- @xfail_eventlet_670
+ @skip_eventlet_670
def test_bug_1176(self) -> None:
self.check_coverage("""\
import asyncio
From 2aa5ffba6f9b575234919080ca45bf0d38808210 Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Tue, 23 Jan 2024 08:30:26 -0500
Subject: [PATCH 31/41] build: skip windows PyPy 3.9 and 3.10 for 7.3.15
---
.github/workflows/testsuite.yml | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/.github/workflows/testsuite.yml b/.github/workflows/testsuite.yml
index 8017afde0..cab43b601 100644
--- a/.github/workflows/testsuite.yml
+++ b/.github/workflows/testsuite.yml
@@ -53,6 +53,14 @@ jobs:
- "pypy-3.8"
- "pypy-3.9"
- "pypy-3.10"
+ exclude:
+ # Windows pypy 3.9 and 3.10 get stuck with PyPy 7.3.15. I hope to
+ # unstick them, but I don't want that to block all other progress, so
+ # skip them for now.
+ - os: windows
+ python-version: "pypy-3.9"
+ - os: windows
+ python-version: "pypy-3.10"
fail-fast: false
steps:
From de60a6d1db6ac3faf4c59bb562b0cfed9f2384a5 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 23 Jan 2024 13:45:29 +0000
Subject: [PATCH 32/41] build(deps): bump actions/dependency-review-action from
3 to 4
Bumps [actions/dependency-review-action](https://github.com/actions/dependency-review-action) from 3 to 4.
- [Release notes](https://github.com/actions/dependency-review-action/releases)
- [Commits](https://github.com/actions/dependency-review-action/compare/v3...v4)
---
updated-dependencies:
- dependency-name: actions/dependency-review-action
dependency-type: direct:production
update-type: version-update:semver-major
...
Signed-off-by: dependabot[bot]
---
.github/workflows/dependency-review.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml
index 248927a6c..c646b2182 100644
--- a/.github/workflows/dependency-review.yml
+++ b/.github/workflows/dependency-review.yml
@@ -25,7 +25,7 @@ jobs:
- name: 'Checkout Repository'
uses: actions/checkout@v4
- name: 'Dependency Review'
- uses: actions/dependency-review-action@v3
+ uses: actions/dependency-review-action@v4
with:
base-ref: ${{ github.event.pull_request.base.sha || 'master' }}
head-ref: ${{ github.event.pull_request.head.sha || github.ref }}
From f8be8652fe997eee3ce25314b8d038158be1367e Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Thu, 25 Jan 2024 07:57:16 -0500
Subject: [PATCH 33/41] build: run actions on 3.13 since a3 came out.
---
.github/workflows/coverage.yml | 1 +
.github/workflows/testsuite.yml | 3 ++-
2 files changed, 3 insertions(+), 1 deletion(-)
diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml
index 1ca3468ad..88aaadec5 100644
--- a/.github/workflows/coverage.yml
+++ b/.github/workflows/coverage.yml
@@ -50,6 +50,7 @@ jobs:
- "3.10"
- "3.11"
- "3.12"
+ - "3.13"
- "pypy-3.8"
- "pypy-3.9"
- "pypy-3.10"
diff --git a/.github/workflows/testsuite.yml b/.github/workflows/testsuite.yml
index cab43b601..9e1c3c1fa 100644
--- a/.github/workflows/testsuite.yml
+++ b/.github/workflows/testsuite.yml
@@ -50,12 +50,13 @@ jobs:
- "3.10"
- "3.11"
- "3.12"
+ - "3.13"
- "pypy-3.8"
- "pypy-3.9"
- "pypy-3.10"
exclude:
# Windows pypy 3.9 and 3.10 get stuck with PyPy 7.3.15. I hope to
- # unstick them, but I don't want that to block all other progress, so
+ # unstick them, but I don't want that to block all other progress, so
# skip them for now.
- os: windows
python-version: "pypy-3.9"
From b7c41a2b7482d61dac596643e01e301ac98d1400 Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Thu, 25 Jan 2024 08:07:32 -0500
Subject: [PATCH 34/41] build: show action environment variables for debugging
---
.github/workflows/coverage.yml | 20 +++++++++++++++++++-
.github/workflows/python-nightly.yml | 1 +
.github/workflows/quality.yml | 7 ++++++-
.github/workflows/testsuite.yml | 9 +++++++--
4 files changed, 33 insertions(+), 4 deletions(-)
diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml
index 88aaadec5..ddb09d150 100644
--- a/.github/workflows/coverage.yml
+++ b/.github/workflows/coverage.yml
@@ -80,6 +80,13 @@ jobs:
#cache: pip
#cache-dependency-path: 'requirements/*.pip'
+ - name: "Show environment"
+ run: |
+ set -xe
+ python -VV
+ python -m site
+ env
+
- name: "Install dependencies"
run: |
echo matrix id: $MATRIX_ID
@@ -132,11 +139,16 @@ jobs:
#cache: pip
#cache-dependency-path: 'requirements/*.pip'
- - name: "Install dependencies"
+ - name: "Show environment"
run: |
set -xe
python -VV
python -m site
+ env
+
+ - name: "Install dependencies"
+ run: |
+ set -xe
python -m pip install -e .
python igor.py zip_mods
@@ -171,10 +183,16 @@ jobs:
runs-on: ubuntu-latest
steps:
+ - name: "Show environment"
+ run: |
+ set -xe
+ env
+
- name: "Compute info for later steps"
id: info
run: |
set -xe
+ env
export SHA10=$(echo ${{ github.sha }} | cut -c 1-10)
export SLUG=$(date +'%Y%m%d')_$SHA10
export REPORT_DIR=reports/$SLUG/htmlcov
diff --git a/.github/workflows/python-nightly.yml b/.github/workflows/python-nightly.yml
index c4bffd278..fd4bdad53 100644
--- a/.github/workflows/python-nightly.yml
+++ b/.github/workflows/python-nightly.yml
@@ -79,6 +79,7 @@ jobs:
python -m site
python -m coverage debug sys
python -m coverage debug pybehave
+ env
- name: "Install dependencies"
run: |
diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml
index 9922b233b..d1c1f311a 100644
--- a/.github/workflows/quality.yml
+++ b/.github/workflows/quality.yml
@@ -92,11 +92,16 @@ jobs:
cache: pip
cache-dependency-path: 'requirements/*.pip'
- - name: "Install dependencies"
+ - name: "Show environment"
run: |
set -xe
python -VV
python -m site
+ env
+
+ - name: "Install dependencies"
+ run: |
+ set -xe
python -m pip install -r requirements/tox.pip
- name: "Tox doc"
diff --git a/.github/workflows/testsuite.yml b/.github/workflows/testsuite.yml
index 9e1c3c1fa..e6742753e 100644
--- a/.github/workflows/testsuite.yml
+++ b/.github/workflows/testsuite.yml
@@ -79,14 +79,19 @@ jobs:
#cache: pip
#cache-dependency-path: 'requirements/*.pip'
- - name: "Install dependencies"
+ - name: "Show environment"
run: |
set -xe
python -VV
python -m site
- python -m pip install -r requirements/tox.pip
# For extreme debugging:
# python -c "import urllib.request as r; exec(r.urlopen('https://bit.ly/pydoctor').read())"
+ env
+
+ - name: "Install dependencies"
+ run: |
+ set -xe
+ python -m pip install -r requirements/tox.pip
- name: "Run tox for ${{ matrix.python-version }}"
run: |
From 75b22f0e24559446ff551acb6ddbaa92bce8add3 Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Thu, 25 Jan 2024 09:24:19 -0500
Subject: [PATCH 35/41] test: ignore color in tracebacks
Python 3.13 adds color escape sequences to tracebacks. GitHub Actions
sets FORCE_COLOR=1, so they appear in color there, but not locally. We
don't care about that difference, so strip the escape sequences.
---
tests/helpers.py | 5 +++++
tests/test_execfile.py | 3 ++-
tests/test_process.py | 11 ++++++++---
tests/test_testing.py | 14 +++++++++++++-
4 files changed, 28 insertions(+), 5 deletions(-)
diff --git a/tests/helpers.py b/tests/helpers.py
index 9e6e2e8de..03e694db2 100644
--- a/tests/helpers.py
+++ b/tests/helpers.py
@@ -397,3 +397,8 @@ def __init__(self, options: Iterable[str]) -> None:
def get_output(self) -> str:
"""Get the output text from the `DebugControl`."""
return self.io.getvalue()
+
+
+def without_color(text: str) -> str:
+ """Remove ANSI color-setting escape sequences."""
+ return re.sub(r"\033\[[\d;]+m", "", text)
diff --git a/tests/test_execfile.py b/tests/test_execfile.py
index 908857942..c3791e635 100644
--- a/tests/test_execfile.py
+++ b/tests/test_execfile.py
@@ -23,6 +23,7 @@
from coverage.files import python_reported_file
from tests.coveragetest import CoverageTest, TESTS_DIR, UsingModulesMixin
+from tests.helpers import without_color
TRY_EXECFILE = os.path.join(TESTS_DIR, "modules/process_test/try_execfile.py")
@@ -188,7 +189,7 @@ def excepthook(*args):
run_python_file(["excepthook_throw.py"])
# The _ExceptionDuringRun exception has the RuntimeError as its argument.
assert exc_info.value.args[1].args[0] == "Error Outside"
- stderr = self.stderr()
+ stderr = without_color(self.stderr())
assert "in excepthook\n" in stderr
assert "Error in sys.excepthook:\n" in stderr
assert "RuntimeError: Error Inside" in stderr
diff --git a/tests/test_process.py b/tests/test_process.py
index 5aeb49744..a5db56b69 100644
--- a/tests/test_process.py
+++ b/tests/test_process.py
@@ -27,7 +27,7 @@
from tests import testenv
from tests.coveragetest import CoverageTest, TESTS_DIR
-from tests.helpers import re_line, re_lines, re_lines_text
+from tests.helpers import re_line, re_lines, re_lines_text, without_color
class ProcessTest(CoverageTest):
@@ -318,6 +318,7 @@ def f2():
assert out == out2
# But also make sure that the output is what we expect.
+ out = without_color(out)
path = python_reported_file('throw.py')
msg = f'File "{re.escape(path)}", line 8, in f2'
assert re.search(msg, out)
@@ -969,8 +970,11 @@ def excepthook(*args):
py_st, py_out = self.run_command_status("python excepthook_throw.py")
assert cov_st == py_st
assert cov_st == 1
- assert "in excepthook" in py_out
- assert cov_out == py_out
+ assert "in excepthook" in without_color(py_out)
+ # Don't know why: the Python output shows "Error in sys.excepthook" and
+ # "Original exception" in color. The coverage output has the first in
+ # color and "Original" without color? Strip all the color.
+ assert without_color(cov_out) == without_color(py_out)
class AliasedCommandTest(CoverageTest):
@@ -1177,6 +1181,7 @@ def test_removing_directory(self) -> None:
def test_removing_directory_with_error(self) -> None:
self.make_file("bug806.py", self.BUG_806)
out = self.run_command("coverage run bug806.py")
+ out = without_color(out)
path = python_reported_file('bug806.py')
# Python 3.11 adds an extra line to the traceback.
# Check that the lines we expect are there.
diff --git a/tests/test_testing.py b/tests/test_testing.py
index 3c3b0622e..1af6613ea 100644
--- a/tests/test_testing.py
+++ b/tests/test_testing.py
@@ -24,7 +24,7 @@
from tests.helpers import (
CheckUniqueFilenames, FailingProxy,
arcs_to_arcz_repr, arcz_to_arcs, assert_count_equal, assert_coverage_warnings,
- re_lines, re_lines_text, re_line,
+ re_lines, re_lines_text, re_line, without_color,
)
@@ -472,3 +472,15 @@ def subtract(self, a, b): # type: ignore[no-untyped-def]
proxy.add(3, 4)
# then add starts working
assert proxy.add(5, 6) == 11
+
+
+@pytest.mark.parametrize("text, result", [
+ ("", ""),
+ ("Nothing to see here", "Nothing to see here"),
+ ("Oh no! \x1b[1;35mRuntimeError\x1b[0m. Fix it.", "Oh no! RuntimeError. Fix it."),
+ ("Fancy: \x1b[48;5;95mBkgd\x1b[38;2;100;200;25mRGB\x1b[0m", "Fancy: BkgdRGB"),
+ # Other escape sequences are unaffected.
+ ("X\x1b[2J\x1b[1mBold\x1b[22m\x1b[=3hZ", "X\x1b[2JBold\x1b[=3hZ"),
+])
+def test_without_color(text: str, result: str) -> None:
+ assert without_color(text) == result
From 498b8c9caca6d0c9fbc10c5f753e4a8521b5c0bc Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Thu, 25 Jan 2024 17:34:31 -0500
Subject: [PATCH 36/41] build: coverage runs have to skip windows pypy too
---
.github/workflows/coverage.yml | 10 +++++++---
1 file changed, 7 insertions(+), 3 deletions(-)
diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml
index ddb09d150..5bd88afa1 100644
--- a/.github/workflows/coverage.yml
+++ b/.github/workflows/coverage.yml
@@ -55,12 +55,16 @@ jobs:
- "pypy-3.9"
- "pypy-3.10"
exclude:
- # Mac PyPy always takes the longest, and doesn't add anything.
+ # Mac PyPy always takes the longest, and doesn't add anything. Skip
+ # 3.8, but use 3.9/3.10 while Windows is still borked.
- os: macos
python-version: "pypy-3.8"
- - os: macos
+ # Windows pypy 3.9 and 3.10 get stuck with PyPy 7.3.15. I hope to
+ # unstick them, but I don't want that to block all other progress, so
+ # skip them for now.
+ - os: windows
python-version: "pypy-3.9"
- - os: macos
+ - os: windows
python-version: "pypy-3.10"
# If one job fails, stop the whole thing.
fail-fast: true
From 98cd6719202e032ee8e6ff35275791cb7f0ec522 Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Fri, 26 Jan 2024 09:14:56 -0500
Subject: [PATCH 37/41] docs: correct two library urls
---
doc/cmd.rst | 4 ++--
doc/config.rst | 4 ++--
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/doc/cmd.rst b/doc/cmd.rst
index 2162cc84e..a440e987c 100644
--- a/doc/cmd.rst
+++ b/doc/cmd.rst
@@ -183,8 +183,8 @@ configuration file for all options.
.. _multiprocessing: https://docs.python.org/3/library/multiprocessing.html
.. _greenlet: https://greenlet.readthedocs.io/
-.. _gevent: http://www.gevent.org/
-.. _eventlet: http://eventlet.net/
+.. _gevent: https://www.gevent.org/
+.. _eventlet: https://eventlet.readthedocs.io/
If you are measuring coverage in a multi-process program, or across a number of
machines, you'll want the ``--parallel-mode`` switch to keep the data separate
diff --git a/doc/config.rst b/doc/config.rst
index 6e645fc2e..540ec780a 100644
--- a/doc/config.rst
+++ b/doc/config.rst
@@ -304,8 +304,8 @@ produce very wrong results.
.. _multiprocessing: https://docs.python.org/3/library/multiprocessing.html
.. _greenlet: https://greenlet.readthedocs.io/
-.. _gevent: http://www.gevent.org/
-.. _eventlet: http://eventlet.net/
+.. _gevent: https://www.gevent.org/
+.. _eventlet: https://eventlet.readthedocs.io/
See :ref:`subprocess` for details of multi-process measurement.
From ddc88f70732c759314d0c7e88e4b46422fd104ab Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Fri, 26 Jan 2024 09:15:58 -0500
Subject: [PATCH 38/41] docs: prep for 7.4.1
---
CHANGES.rst | 12 ++++++++----
NOTICE.txt | 2 +-
coverage/version.py | 4 ++--
doc/conf.py | 8 ++++----
4 files changed, 15 insertions(+), 11 deletions(-)
diff --git a/CHANGES.rst b/CHANGES.rst
index 55384553b..a65101ea6 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -17,8 +17,14 @@ development at the same time, such as 4.5.x and 5.0.
.. Version 9.8.1 — 2027-07-27
.. --------------------------
-Unreleased
-----------
+.. scriv-start-here
+
+.. _changes_7-4-1:
+
+Version 7.4.1 — 2024-01-26
+--------------------------
+
+- Python 3.13.0a3 is supported.
- Fix: the JSON report now includes an explicit format version number, closing
`issue 1732`_.
@@ -26,8 +32,6 @@ Unreleased
.. _issue 1732: https://github.com/nedbat/coveragepy/issues/1732
-.. scriv-start-here
-
.. _changes_7-4-0:
Version 7.4.0 — 2023-12-27
diff --git a/NOTICE.txt b/NOTICE.txt
index 68810cd4e..7376ffdda 100644
--- a/NOTICE.txt
+++ b/NOTICE.txt
@@ -1,5 +1,5 @@
Copyright 2001 Gareth Rees. All rights reserved.
-Copyright 2004-2023 Ned Batchelder. All rights reserved.
+Copyright 2004-2024 Ned Batchelder. All rights reserved.
Except where noted otherwise, this software is licensed under the Apache
License, Version 2.0 (the "License"); you may not use this work except in
diff --git a/coverage/version.py b/coverage/version.py
index b8541c8b7..2f953e709 100644
--- a/coverage/version.py
+++ b/coverage/version.py
@@ -8,8 +8,8 @@
# version_info: same semantics as sys.version_info.
# _dev: the .devN suffix if any.
-version_info = (7, 4, 1, "alpha", 0)
-_dev = 1
+version_info = (7, 4, 1, "final", 0)
+_dev = 0
def _make_version(
diff --git a/doc/conf.py b/doc/conf.py
index db21f3495..04ce4c0a6 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -65,13 +65,13 @@
# built documents.
# @@@ editable
-copyright = "2009–2023, Ned Batchelder" # pylint: disable=redefined-builtin
+copyright = "2009–2024, Ned Batchelder" # pylint: disable=redefined-builtin
# The short X.Y.Z version.
-version = "7.4.0"
+version = "7.4.1"
# The full version, including alpha/beta/rc tags.
-release = "7.4.0"
+release = "7.4.1"
# The date of release, in "monthname day, year" format.
-release_date = "December 27, 2023"
+release_date = "January 26, 2024"
# @@@ end
rst_epilog = """
From 8d1857f73401b0b3d744a2f458621eb435b44a1c Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Fri, 26 Jan 2024 09:16:22 -0500
Subject: [PATCH 39/41] docs: sample HTML for 7.4.1
---
doc/sample_html/d_7b071bdc2a35fa80___init___py.html | 8 ++++----
doc/sample_html/d_7b071bdc2a35fa80___main___py.html | 8 ++++----
doc/sample_html/d_7b071bdc2a35fa80_cogapp_py.html | 8 ++++----
doc/sample_html/d_7b071bdc2a35fa80_makefiles_py.html | 8 ++++----
doc/sample_html/d_7b071bdc2a35fa80_test_cogapp_py.html | 8 ++++----
doc/sample_html/d_7b071bdc2a35fa80_test_makefiles_py.html | 8 ++++----
.../d_7b071bdc2a35fa80_test_whiteutils_py.html | 8 ++++----
doc/sample_html/d_7b071bdc2a35fa80_utils_py.html | 8 ++++----
doc/sample_html/d_7b071bdc2a35fa80_whiteutils_py.html | 8 ++++----
doc/sample_html/index.html | 8 ++++----
doc/sample_html/status.json | 2 +-
11 files changed, 41 insertions(+), 41 deletions(-)
diff --git a/doc/sample_html/d_7b071bdc2a35fa80___init___py.html b/doc/sample_html/d_7b071bdc2a35fa80___init___py.html
index 1fc60c859..f6a0e5eef 100644
--- a/doc/sample_html/d_7b071bdc2a35fa80___init___py.html
+++ b/doc/sample_html/d_7b071bdc2a35fa80___init___py.html
@@ -66,8 +66,8 @@