From 97e68a0cf89751b1dff74106ce555ce4022c907c Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 16 Mar 2025 14:05:29 -0400 Subject: [PATCH 1/7] build: bump version to 7.7.1 --- CHANGES.rst | 6 ++++++ coverage/version.py | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 27b90e8ca..2ba51b6b2 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -20,6 +20,12 @@ upgrading your version of coverage.py. .. Version 9.8.1 — 2027-07-27 .. -------------------------- +Unreleased +---------- + +Nothing yet. + + .. start-releases .. _changes_7-7-0: diff --git a/coverage/version.py b/coverage/version.py index d9c814811..e6e80bf51 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, 7, 0, "final", 0) -_dev = 0 +version_info = (7, 7, 1, "alpha", 0) +_dev = 1 def _make_version( From f503dc5285692c6073de6299898738ec2b2d6006 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 20 Mar 2025 08:26:32 -0400 Subject: [PATCH 2/7] perf: collect more stats in sysmon --- coverage/collector.py | 2 +- coverage/sysmon.py | 19 ++++++++++++++++--- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/coverage/collector.py b/coverage/collector.py index 53fa6871c..3f1519a98 100644 --- a/coverage/collector.py +++ b/coverage/collector.py @@ -367,7 +367,7 @@ def pause(self) -> None: tracer.stop() stats = tracer.get_stats() if stats: - print("\nCoverage.py tracer stats:") + print(f"\nCoverage.py {tracer.__class__.__name__} stats:") for k, v in human_sorted_items(stats.items()): print(f"{k:>20}: {v}") if self.threading: diff --git a/coverage/sysmon.py b/coverage/sysmon.py index 805b5f9a3..118fecdb5 100644 --- a/coverage/sysmon.py +++ b/coverage/sysmon.py @@ -369,9 +369,10 @@ def __init__(self, tool_id: int) -> None: self.stats: dict[str, int] | None = None if COLLECT_STATS: - self.stats = { - "starts": 0, - } + self.stats = dict.fromkeys( + "starts start_tracing returns line_lines line_arcs branches branch_trails".split(), + 0, + ) self.stopped = False self._activity = False @@ -493,6 +494,8 @@ def sysmon_py_start( self.code_objects.append(code) if tracing_code: + if self.stats is not None: + self.stats["start_tracing"] += 1 events = sys.monitoring.events with self.lock: if self.sysmon_on: @@ -516,6 +519,8 @@ def sysmon_py_return( retval: object, ) -> MonitorReturn: """Handle sys.monitoring.events.PY_RETURN events for branch coverage.""" + if self.stats is not None: + self.stats["returns"] += 1 code_info = self.code_infos.get(id(code)) if code_info is not None and code_info.file_data is not None: assert code_info.byte_to_line is not None @@ -529,6 +534,8 @@ def sysmon_py_return( @panopticon("code", "line") def sysmon_line_lines(self, code: CodeType, line_number: TLineNo) -> MonitorReturn: """Handle sys.monitoring.events.LINE events for line coverage.""" + if self.stats is not None: + self.stats["line_lines"] += 1 code_info = self.code_infos.get(id(code)) if code_info is not None and code_info.file_data is not None: cast(set[TLineNo], code_info.file_data).add(line_number) @@ -538,6 +545,8 @@ def sysmon_line_lines(self, code: CodeType, line_number: TLineNo) -> MonitorRetu @panopticon("code", "line") def sysmon_line_arcs(self, code: CodeType, line_number: TLineNo) -> MonitorReturn: """Handle sys.monitoring.events.LINE events for branch coverage.""" + if self.stats is not None: + self.stats["line_arcs"] += 1 code_info = self.code_infos[id(code)] if code_info.file_data is not None: arc = (line_number, line_number) @@ -550,9 +559,13 @@ def sysmon_branch_either( self, code: CodeType, instruction_offset: TOffset, destination_offset: TOffset ) -> MonitorReturn: """Handle BRANCH_RIGHT and BRANCH_LEFT events.""" + if self.stats is not None: + self.stats["branches"] += 1 code_info = self.code_infos[id(code)] if code_info.file_data is not None: if not code_info.branch_trails: + if self.stats is not None: + self.stats["branch_trails"] += 1 populate_branch_trails(code, code_info) # log(f"branch_trails for {code}:\n {code_info.branch_trails}") added_arc = False From 7ea1535f7ed5abc1aea9a65bbd557b8f20b2346f Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 20 Mar 2025 08:39:59 -0400 Subject: [PATCH 3/7] refactor: remove some needless checks --- coverage/sysmon.py | 87 ++++++++++++++++++++++++---------------------- 1 file changed, 46 insertions(+), 41 deletions(-) diff --git a/coverage/sysmon.py b/coverage/sysmon.py index 118fecdb5..e7b5659a6 100644 --- a/coverage/sysmon.py +++ b/coverage/sysmon.py @@ -522,13 +522,13 @@ def sysmon_py_return( if self.stats is not None: self.stats["returns"] += 1 code_info = self.code_infos.get(id(code)) - if code_info is not None and code_info.file_data is not None: - assert code_info.byte_to_line is not None - last_line = code_info.byte_to_line[instruction_offset] - if last_line is not None: - arc = (last_line, -code.co_firstlineno) - cast(set[TArc], code_info.file_data).add(arc) - # log(f"adding {arc=}") + # code_info is not None and code_info.file_data is not None, since we + # wouldn't have enabled this event if they were. + last_line = code_info.byte_to_line[instruction_offset] # type: ignore + if last_line is not None: + arc = (last_line, -code.co_firstlineno) + code_info.file_data.add(arc) # type: ignore + # log(f"adding {arc=}") return DISABLE @panopticon("code", "line") @@ -537,9 +537,12 @@ def sysmon_line_lines(self, code: CodeType, line_number: TLineNo) -> MonitorRetu if self.stats is not None: self.stats["line_lines"] += 1 code_info = self.code_infos.get(id(code)) + # It should be true that code_info is not None and code_info.file_data + # is not None, since we wouldn't have enabled this event if they were. + # But somehow code_info can be None here, so we have to check. if code_info is not None and code_info.file_data is not None: - cast(set[TLineNo], code_info.file_data).add(line_number) - # log(f"adding {line_number=}") + code_info.file_data.add(line_number) # type: ignore + # log(f"adding {line_number=}") return DISABLE @panopticon("code", "line") @@ -548,10 +551,11 @@ def sysmon_line_arcs(self, code: CodeType, line_number: TLineNo) -> MonitorRetur if self.stats is not None: self.stats["line_arcs"] += 1 code_info = self.code_infos[id(code)] - if code_info.file_data is not None: - arc = (line_number, line_number) - cast(set[TArc], code_info.file_data).add(arc) - # log(f"adding {arc=}") + # code_info is not None and code_info.file_data is not None, since we + # wouldn't have enabled this event if they were. + arc = (line_number, line_number) + code_info.file_data.add(arc) # type: ignore + # log(f"adding {arc=}") return DISABLE @panopticon("code", "@", "@") @@ -562,33 +566,34 @@ def sysmon_branch_either( if self.stats is not None: self.stats["branches"] += 1 code_info = self.code_infos[id(code)] - if code_info.file_data is not None: - if not code_info.branch_trails: - if self.stats is not None: - self.stats["branch_trails"] += 1 - populate_branch_trails(code, code_info) - # log(f"branch_trails for {code}:\n {code_info.branch_trails}") - added_arc = False - dest_info = code_info.branch_trails.get(instruction_offset) - # log(f"{dest_info = }") - if dest_info is not None: - for offsets, arc in dest_info: - if arc is None: - continue - if destination_offset in offsets: - cast(set[TArc], code_info.file_data).add(arc) - # log(f"adding {arc=}") - added_arc = True - break - - if not added_arc: - # This could be an exception jumping from line to line. - assert code_info.byte_to_line is not None - l1 = code_info.byte_to_line[instruction_offset] - l2 = code_info.byte_to_line[destination_offset] - if l1 != l2: - arc = (l1, l2) - cast(set[TArc], code_info.file_data).add(arc) - # log(f"adding unforeseen {arc=}") + # code_info is not None and code_info.file_data is not None, since we + # wouldn't have enabled this event if they were. + if not code_info.branch_trails: + if self.stats is not None: + self.stats["branch_trails"] += 1 + populate_branch_trails(code, code_info) + # log(f"branch_trails for {code}:\n {code_info.branch_trails}") + added_arc = False + dest_info = code_info.branch_trails.get(instruction_offset) + # log(f"{dest_info = }") + if dest_info is not None: + for offsets, arc in dest_info: + if arc is None: + continue + if destination_offset in offsets: + code_info.file_data.add(arc) # type: ignore + # log(f"adding {arc=}") + added_arc = True + break + + if not added_arc: + # This could be an exception jumping from line to line. + assert code_info.byte_to_line is not None + l1 = code_info.byte_to_line[instruction_offset] + l2 = code_info.byte_to_line[destination_offset] + if l1 != l2: + arc = (l1, l2) + code_info.file_data.add(arc) # type: ignore + # log(f"adding unforeseen {arc=}") return DISABLE From 87bc26bc1f0148ba9ff746d8b82584ffecdc67f8 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 20 Mar 2025 15:41:23 -0400 Subject: [PATCH 4/7] refactor: use f-strings more --- coverage/config.py | 2 +- coverage/context.py | 7 +++---- coverage/debug.py | 4 ++-- coverage/execfile.py | 2 +- coverage/files.py | 2 +- coverage/parser.py | 8 ++++---- coverage/sqldata.py | 2 +- 7 files changed, 13 insertions(+), 14 deletions(-) diff --git a/coverage/config.py b/coverage/config.py index a8366d199..75f314816 100644 --- a/coverage/config.py +++ b/coverage/config.py @@ -449,7 +449,7 @@ def _set_attr_from_config_option( """ section, option = where.split(":") if cp.has_option(section, option): - method = getattr(cp, "get" + type_) + method = getattr(cp, f"get{type_}") setattr(self, attr, method(section, option)) return True return False diff --git a/coverage/context.py b/coverage/context.py index 977e9b4ef..2921372a8 100644 --- a/coverage/context.py +++ b/coverage/context.py @@ -6,7 +6,6 @@ from __future__ import annotations from types import FrameType -from typing import cast from collections.abc import Sequence from coverage.types import TShouldStartContextFn @@ -65,11 +64,11 @@ def qualname_from_frame(frame: FrameType) -> str | None: func = frame.f_globals.get(fname) if func is None: return None - return cast(str, func.__module__ + "." + fname) + return f"{func.__module__}.{fname}" func = getattr(method, "__func__", None) if func is None: cls = self.__class__ - return cast(str, cls.__module__ + "." + cls.__name__ + "." + fname) + return f"{cls.__module__}.{cls.__name__}.{fname}" - return cast(str, func.__module__ + "." + func.__qualname__) + return f"{func.__module__}.{func.__qualname__}" diff --git a/coverage/debug.py b/coverage/debug.py index fbd500a72..73e842f99 100644 --- a/coverage/debug.py +++ b/coverage/debug.py @@ -362,7 +362,7 @@ def filter(self, text: str) -> str: """Add a cwd message for each new cwd.""" cwd = os.getcwd() if cwd != self.cwd: - text = f"cwd is now {cwd!r}\n" + text + text = f"cwd is now {cwd!r}\n{text}" self.cwd = cwd return text @@ -404,7 +404,7 @@ def filter(self, text: str) -> str: """Add a message when the pytest test changes.""" test_name = os.getenv("PYTEST_CURRENT_TEST") if test_name != self.test_name: - text = f"Pytest context: {test_name}\n" + text + text = f"Pytest context: {test_name}\n{text}" self.test_name = test_name return text diff --git a/coverage/execfile.py b/coverage/execfile.py index cbecec847..0affda498 100644 --- a/coverage/execfile.py +++ b/coverage/execfile.py @@ -142,7 +142,7 @@ def _prepare2(self) -> None: # Running a directory means running the __main__.py file in that # directory. for ext in [".py", ".pyc", ".pyo"]: - try_filename = os.path.join(self.arg0, "__main__" + ext) + try_filename = os.path.join(self.arg0, f"__main__{ext}") # 3.8.10 changed how files are reported when running a # directory. try_filename = os.path.abspath(try_filename) diff --git a/coverage/files.py b/coverage/files.py index 15d39acbd..21ba3f167 100644 --- a/coverage/files.py +++ b/coverage/files.py @@ -332,7 +332,7 @@ def _glob_to_regex(pattern: str) -> str: # Turn all backslashes into slashes to simplify the tokenizer. pattern = pattern.replace("\\", "/") if "/" not in pattern: - pattern = "**/" + pattern + pattern = f"**/{pattern}" path_rx = [] pos = 0 while pos < len(pattern): diff --git a/coverage/parser.py b/coverage/parser.py index 431ae829e..306123b47 100644 --- a/coverage/parser.py +++ b/coverage/parser.py @@ -741,7 +741,7 @@ def analyze(self) -> None: """Examine the AST tree from `self.root_node` to determine possible arcs.""" for node in ast.walk(self.root_node): node_name = node.__class__.__name__ - code_object_handler = getattr(self, "_code_object__" + node_name, None) + code_object_handler = getattr(self, f"_code_object__{node_name}", None) if code_object_handler is not None: code_object_handler(node) @@ -832,7 +832,7 @@ def line_for_node(self, node: ast.AST) -> TLineNo: node_name = node.__class__.__name__ handler = cast( Optional[Callable[[ast.AST], TLineNo]], - getattr(self, "_line__" + node_name, None), + getattr(self, f"_line__{node_name}", None), ) if handler is not None: line = handler(node) @@ -913,7 +913,7 @@ def node_exits(self, node: ast.AST) -> set[ArcStart]: node_name = node.__class__.__name__ handler = cast( Optional[Callable[[ast.AST], set[ArcStart]]], - getattr(self, "_handle__" + node_name, None), + getattr(self, f"_handle__{node_name}", None), ) if handler is not None: arc_starts = handler(node) @@ -989,7 +989,7 @@ def find_non_missing_node(self, node: ast.AST) -> ast.AST | None: missing_fn = cast( Optional[Callable[[ast.AST], Optional[ast.AST]]], - getattr(self, "_missing__" + node.__class__.__name__, None), + getattr(self, f"_missing__{node.__class__.__name__}", None), ) if missing_fn is not None: ret_node = missing_fn(node) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index 76b569285..169649f3a 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -266,7 +266,7 @@ def _choose_filename(self) -> None: self._filename = self._basename suffix = filename_suffix(self._suffix) if suffix: - self._filename += "." + suffix + self._filename += f".{suffix}" def _reset(self) -> None: """Reset our attributes.""" From 1be53a8083146ae80471e72486dbe7da6f1c98ff Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 20 Mar 2025 15:43:52 -0400 Subject: [PATCH 5/7] docs: add clarification about missing line numbers in the text report --- doc/cmd.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/doc/cmd.rst b/doc/cmd.rst index 3629322e2..74113549b 100644 --- a/doc/cmd.rst +++ b/doc/cmd.rst @@ -600,6 +600,12 @@ detail the missed branches:: --------------------------------------------------------------------- TOTAL 91 12 18 3 87% +Ranges of lines are shown with a dash: "17-23" means all lines from 17 to 23 +inclusive are missing coverage. Missed branches are shown with an arrow: +"40->45" means the branch from line 40 to line 45 is missing. A branch can go +backwards in a file, so you might see a branch from a later line to an earlier +line, like "55->50". + You can restrict the report to only certain files by naming them on the command line:: From 9b82965ff218ea13a63386b585814bda67e542ef Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 21 Mar 2025 12:53:20 -0400 Subject: [PATCH 6/7] docs: prep for 7.7.1 --- CHANGES.rst | 11 +++++++---- coverage/version.py | 4 ++-- doc/conf.py | 6 +++--- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 2ba51b6b2..4d3e07702 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -20,13 +20,16 @@ upgrading your version of coverage.py. .. Version 9.8.1 — 2027-07-27 .. -------------------------- -Unreleased ----------- +.. start-releases + +.. _changes_7-7-1: -Nothing yet. +Version 7.7.1 — 2025-03-21 +-------------------------- +- A few small tweaks to the sys.monitoring support for Python 3.14. Please + test! -.. start-releases .. _changes_7-7-0: diff --git a/coverage/version.py b/coverage/version.py index e6e80bf51..b14eab49f 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, 7, 1, "alpha", 0) -_dev = 1 +version_info = (7, 7, 1, "final", 0) +_dev = 0 def _make_version( diff --git a/doc/conf.py b/doc/conf.py index f94d30f37..80fc2cca8 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -67,11 +67,11 @@ # @@@ editable copyright = "2009–2025, Ned Batchelder" # pylint: disable=redefined-builtin # The short X.Y.Z version. -version = "7.7.0" +version = "7.7.1" # The full version, including alpha/beta/rc tags. -release = "7.7.0" +release = "7.7.1" # The date of release, in "monthname day, year" format. -release_date = "March 16, 2025" +release_date = "March 21, 2025" # @@@ end rst_epilog = f""" From 5e0fd514aa9d49d39afc9b1e57008c20c6c45663 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 21 Mar 2025 12:54:01 -0400 Subject: [PATCH 7/7] docs: sample HTML for 7.7.1 --- doc/sample_html/class_index.html | 8 ++++---- doc/sample_html/function_index.html | 8 ++++---- doc/sample_html/index.html | 8 ++++---- doc/sample_html/status.json | 2 +- doc/sample_html/z_7b071bdc2a35fa80___init___py.html | 8 ++++---- doc/sample_html/z_7b071bdc2a35fa80___main___py.html | 8 ++++---- doc/sample_html/z_7b071bdc2a35fa80_cogapp_py.html | 8 ++++---- doc/sample_html/z_7b071bdc2a35fa80_makefiles_py.html | 8 ++++---- doc/sample_html/z_7b071bdc2a35fa80_test_cogapp_py.html | 8 ++++---- doc/sample_html/z_7b071bdc2a35fa80_test_makefiles_py.html | 8 ++++---- .../z_7b071bdc2a35fa80_test_whiteutils_py.html | 8 ++++---- doc/sample_html/z_7b071bdc2a35fa80_utils_py.html | 8 ++++---- doc/sample_html/z_7b071bdc2a35fa80_whiteutils_py.html | 8 ++++---- 13 files changed, 49 insertions(+), 49 deletions(-) diff --git a/doc/sample_html/class_index.html b/doc/sample_html/class_index.html index d41b6c268..796683503 100644 --- a/doc/sample_html/class_index.html +++ b/doc/sample_html/class_index.html @@ -56,8 +56,8 @@

Classes

- coverage.py v7.7.0, - created at 2025-03-16 13:28 -0400 + coverage.py v7.7.1, + created at 2025-03-21 12:53 -0400

@@ -537,8 +537,8 @@

- coverage.py v7.7.0, - created at 2025-03-16 13:28 -0400 + coverage.py v7.7.1, + created at 2025-03-21 12:53 -0400

diff --git a/doc/sample_html/z_7b071bdc2a35fa80_makefiles_py.html b/doc/sample_html/z_7b071bdc2a35fa80_makefiles_py.html index d89a20036..40f9b68ae 100644 --- a/doc/sample_html/z_7b071bdc2a35fa80_makefiles_py.html +++ b/doc/sample_html/z_7b071bdc2a35fa80_makefiles_py.html @@ -66,8 +66,8 @@

^ index     » next       - coverage.py v7.7.0, - created at 2025-03-16 13:28 -0400 + coverage.py v7.7.1, + created at 2025-03-21 12:53 -0400