diff --git a/CHANGES.rst b/CHANGES.rst index cf338adf4..bc879fb55 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -23,7 +23,10 @@ upgrading your version of coverage.py. Unreleased ---------- -Nothing yet. +- fix: multi-line ``with`` statements could cause contained branches to be + incorrectly marked as missing (`issue 1880`_). This is now fixed. + +.. _issue 1880: https://github.com/nedbat/coveragepy/issues/1880 .. start-releases diff --git a/coverage/env.py b/coverage/env.py index 0f3a389d2..39ec169c2 100644 --- a/coverage/env.py +++ b/coverage/env.py @@ -81,6 +81,10 @@ class PYBEHAVIOR: # When leaving a with-block, do we visit the with-line again for the exit? exit_through_with = (PYVERSION >= (3, 10, 0, "beta")) + # When leaving a with-block, do we visit the with-line exactly, + # or the inner-most context manager? + exit_with_through_ctxmgr = (PYVERSION >= (3, 12)) + # Match-case construct. match_case = (PYVERSION >= (3, 10)) diff --git a/coverage/parser.py b/coverage/parser.py index 877bb5e55..c68fb4eaf 100644 --- a/coverage/parser.py +++ b/coverage/parser.py @@ -1261,12 +1261,19 @@ def _handle__While(self, node: ast.While) -> set[ArcStart]: return exits def _handle__With(self, node: ast.With) -> set[ArcStart]: - start = self.line_for_node(node) + if env.PYBEHAVIOR.exit_with_through_ctxmgr: + starts = [self.line_for_node(item.context_expr) for item in node.items] + else: + starts = [self.line_for_node(node)] if env.PYBEHAVIOR.exit_through_with: - self.current_with_starts.add(start) - self.all_with_starts.add(start) - exits = self.process_body(node.body, from_start=ArcStart(start)) + for start in starts: + self.current_with_starts.add(start) + self.all_with_starts.add(start) + + exits = self.process_body(node.body, from_start=ArcStart(starts[-1])) + if env.PYBEHAVIOR.exit_through_with: + start = starts[-1] self.current_with_starts.remove(start) with_exit = {ArcStart(start)} if exits: diff --git a/tests/test_arcs.py b/tests/test_arcs.py index 969619667..3146daf77 100644 --- a/tests/test_arcs.py +++ b/tests/test_arcs.py @@ -347,6 +347,41 @@ def test_with_with_lambda(self) -> None: branchz_missing="", ) + def test_multiline_with(self) -> None: + # https://github.com/nedbat/coveragepy/issues/1880 + self.check_coverage("""\ + import contextlib, itertools + nums = itertools.count() + with ( + contextlib.nullcontext() as x, + ): + while next(nums) < 6: + y = 7 + z = 8 + """, + branchz="67 68", + branchz_missing="", + ) + + + def test_multi_multiline_with(self) -> None: + # https://github.com/nedbat/coveragepy/issues/1880 + self.check_coverage("""\ + import contextlib, itertools + nums = itertools.count() + with ( + contextlib.nullcontext() as x, + contextlib.nullcontext() as y, + contextlib.nullcontext() as z, + ): + while next(nums) < 8: + y = 9 + z = 10 + """, + branchz="89 8A", + branchz_missing="", + ) + class LoopArcTest(CoverageTest): """Arc-measuring tests involving loops."""