From 1c50ba4a97c3eb04d538ada005b96ffdb40984c0 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 14 Aug 2021 19:13:51 -0400 Subject: [PATCH] fix: missing exceptions through with statements in 3.10 aren't considered missing branches. #1205 --- CHANGES.rst | 5 ++++ coverage/parser.py | 12 +++----- tests/coveragetest.py | 8 ++++++ tests/test_arcs.py | 64 +++++++++++++++++++++++++++++++++++++++---- 4 files changed, 75 insertions(+), 14 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 0fc84cb33..4baa8a368 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -37,6 +37,10 @@ Unreleased now warnings, to ease the use of coverage across versions. Fixes `issue 1035`_. +- Fix handling of exceptions through context managers in Python 3.10. A missing + exception is no longer considered a missing branch from the with statement. + Fixes `issue 1205`_. + - Fix another rarer instance of "Error binding parameter 0 - probably unsupported type." (`issue 1010`_). @@ -44,6 +48,7 @@ Unreleased .. _issue 1105: https://github.com/nedbat/coveragepy/issues/1105 .. _issue 1163: https://github.com/nedbat/coveragepy/issues/1163 .. _issue 1195: https://github.com/nedbat/coveragepy/issues/1195 +.. _issue 1205: https://github.com/nedbat/coveragepy/issues/1205 .. _changes_60b1: diff --git a/coverage/parser.py b/coverage/parser.py index 8d4e9ffbd..8792d0ac0 100644 --- a/coverage/parser.py +++ b/coverage/parser.py @@ -556,14 +556,14 @@ def __init__(self, start): # that need to go through the with-statement while exiting. self.break_from = set() self.continue_from = set() - self.raise_from = set() self.return_from = set() - def _process_exits(self, exits, add_arc, from_set): + def _process_exits(self, exits, add_arc, from_set=None): """Helper to process the four kinds of exits.""" for xit in exits: add_arc(xit.lineno, self.start, xit.cause) - from_set.update(exits) + if from_set is not None: + from_set.update(exits) return True def process_break_exits(self, exits, add_arc): @@ -573,7 +573,7 @@ def process_continue_exits(self, exits, add_arc): return self._process_exits(exits, add_arc, self.continue_from) def process_raise_exits(self, exits, add_arc): - return self._process_exits(exits, add_arc, self.raise_from) + return self._process_exits(exits, add_arc) def process_return_exits(self, exits, add_arc): return self._process_exits(exits, add_arc, self.return_from) @@ -1232,10 +1232,6 @@ def _handle__With(self, node): self.process_continue_exits( self._combine_finally_starts(with_block.continue_from, with_exit) ) - if with_block.raise_from: - self.process_raise_exits( - self._combine_finally_starts(with_block.raise_from, with_exit) - ) if with_block.return_from: self.process_return_exits( self._combine_finally_starts(with_block.return_from, with_exit) diff --git a/tests/coveragetest.py b/tests/coveragetest.py index 8541ed28f..aae4df08a 100644 --- a/tests/coveragetest.py +++ b/tests/coveragetest.py @@ -433,6 +433,14 @@ def get_measured_filenames(self, coverage_data): return {os.path.basename(filename): filename for filename in coverage_data.measured_files()} + def get_missing_arc_description(self, cov, start, end): + """Get the missing-arc description for a line arc in a coverage run.""" + # ugh, unexposed methods?? + filename = self.last_module_name + ".py" + fr = cov._get_file_reporter(filename) + arcs_executed = cov._analyze(filename).arcs_executed() + return fr.missing_arc_description(start, end, arcs_executed) + class UsingModulesMixin: """A mixin for importing modules from tests/modules and tests/moremodules.""" diff --git a/tests/test_arcs.py b/tests/test_arcs.py index b2d75377b..2806f3059 100644 --- a/tests/test_arcs.py +++ b/tests/test_arcs.py @@ -279,6 +279,62 @@ def test_continue_through_with(self): arcz=arcz, ) + def test_raise_through_with(self): + if env.PYBEHAVIOR.exit_through_with: + arcz = ".1 12 27 78 8. 9A A. -23 34 45 53 6-2" + arcz_missing = "6-2 8." + arcz_unpredicted = "3-2 89" + else: + arcz = ".1 12 27 78 8. 9A A. -23 34 45 5-2 6-2" + arcz_missing = "6-2 8." + arcz_unpredicted = "89" + cov = self.check_coverage("""\ + from contextlib import nullcontext + def f(x): + with nullcontext(): + print(4) + raise Exception("Boo6") + print(6) + try: + f(8) + except Exception: + print("oops 10") + """, + arcz=arcz, + arcz_missing=arcz_missing, + arcz_unpredicted=arcz_unpredicted, + ) + expected = "line 3 didn't jump to the function exit" + assert self.get_missing_arc_description(cov, 3, -2) == expected + + def test_untaken_raise_through_with(self): + if env.PYBEHAVIOR.exit_through_with: + #arcz = ".1 12 28 89 9. AB B. -23 3-2 34 45 56 53 63 37 7-2" + arcz = ".1 12 28 89 9. AB B. -23 34 45 56 53 63 37 7-2" + #arcz_missing = "3-2 56 63 AB B." + arcz_missing = "56 63 AB B." + else: + arcz = ".1 12 28 89 9. AB B. -23 34 45 56 6-2 57 7-2" + arcz_missing = "56 6-2 AB B." + cov = self.check_coverage("""\ + from contextlib import nullcontext + def f(x): + with nullcontext(): + print(4) + if x == 5: + raise Exception("Boo6") + print(7) + try: + f(9) + except Exception: + print("oops 11") + """, + arcz=arcz, + arcz_missing=arcz_missing, + ) + expected = "line 3 didn't jump to the function exit" + assert self.get_missing_arc_description(cov, 3, -2) == expected + class LoopArcTest(CoverageTest): """Arc-measuring tests involving loops.""" @@ -1583,14 +1639,10 @@ def f(a, b): arcz=".1 17 7. .2 23 34 45 5. -22 2-2 -33 3-3 -44 4-4", arcz_missing="3-3 -44 4-4", ) - # ugh, unexposed methods?? - filename = self.last_module_name + ".py" - fr = cov._get_file_reporter(filename) - arcs_executed = cov._analyze(filename).arcs_executed() expected = "line 3 didn't finish the generator expression on line 3" - assert expected == fr.missing_arc_description(3, -3, arcs_executed) + assert self.get_missing_arc_description(cov, 3, -3) == expected expected = "line 4 didn't run the generator expression on line 4" - assert expected == fr.missing_arc_description(4, -4, arcs_executed) + assert self.get_missing_arc_description(cov, 4, -4) == expected class DecoratorArcTest(CoverageTest):