From 07b76b251138dd9bc8a2e2cd585fe822dc36785a Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 19 Dec 2023 15:56:52 -0500 Subject: [PATCH] fix: some clause exclusions were broken #1713 --- CHANGES.rst | 5 ++++ coverage/parser.py | 12 ++++------ tests/coveragetest.py | 2 +- tests/test_coverage.py | 53 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 63 insertions(+), 9 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 6ce5abc25..d73cf6ea4 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -20,9 +20,14 @@ development at the same time, such as 4.5.x and 5.0. Unreleased ---------- +- Fix: the change for multi-line signature exclusions in 7.3.3 broke other + forms of nested clauses being excluded properly. This is now fixed, closing + `issue 1713`_. + - Fix: in the HTML report, selecting code for copying won't select the line numbers also. Thanks, `Robert Harris `_. +.. _issue 1713: https://github.com/nedbat/coveragepy/issues/1713 .. _pull 1717: https://github.com/nedbat/coveragepy/pull/1717 diff --git a/coverage/parser.py b/coverage/parser.py index 0aa7a81a7..69019efef 100644 --- a/coverage/parser.py +++ b/coverage/parser.py @@ -158,7 +158,10 @@ def _raw_parse(self) -> None: self.raw_classdefs.add(slineno) elif toktype == token.OP: if ttext == ":" and nesting == 0: - should_exclude = (elineno in self.raw_excluded) or excluding_decorators + should_exclude = ( + self.raw_excluded.intersection(range(first_line, elineno + 1)) + or excluding_decorators + ) if not excluding and should_exclude: # Start excluding a suite. We trigger off of the colon # token so that the #pragma comment will be recognized on @@ -190,12 +193,6 @@ def _raw_parse(self) -> None: # so record a multi-line range. for l in range(first_line, elineno+1): # type: ignore[unreachable] self._multiline[l] = first_line - # Check if multi-line was before a suite (trigger by the colon token). - if nesting == 0 and prev_toktype == token.OP and prev_ttext == ":": - statement_multilines = set(range(first_line, elineno + 1)) - if statement_multilines & set(self.raw_excluded): - exclude_indent = indent - excluding = True first_line = None first_on_line = True @@ -213,7 +210,6 @@ def _raw_parse(self) -> None: first_on_line = False prev_toktype = toktype - prev_ttext = ttext # Find the starts of the executable statements. if not empty: diff --git a/tests/coveragetest.py b/tests/coveragetest.py index 4bfeaeac2..eb2677494 100644 --- a/tests/coveragetest.py +++ b/tests/coveragetest.py @@ -193,7 +193,7 @@ def check_coverage( # Get the analysis results, and check that they are right. analysis = cov._analyze(mod) statements = sorted(analysis.statements) - if lines is not None and len(lines) != 0: + if lines: if isinstance(lines[0], int): # lines is just a list of numbers, it must match the statements # found in the code. diff --git a/tests/test_coverage.py b/tests/test_coverage.py index 1bbb22ac7..1752d5a6e 100644 --- a/tests/test_coverage.py +++ b/tests/test_coverage.py @@ -1740,6 +1740,59 @@ def my_func_2(super_long_input_argument_0=0, super_long_input_argument_1=1, supe [], "5", excludes=['my_func'] ) + def test_excluding_bug1713(self) -> None: + if env.PYVERSION >= (3, 10): + self.check_coverage("""\ + print("1") + + def hello_3(a): # pragma: no cover + match a: + case ("5" + | "6"): + print("7") + case "8": + print("9") + + print("11") + """, + [1, 11], + ) + self.check_coverage("""\ + print("1") + + def hello_3(a): # no thanks + if ("4" or + "5"): + print("6") + else: + print("8") + + print("10") + """, + [1, 10], "", excludes=["no thanks"], + ) + self.check_coverage("""\ + print(1) + + def func(a, b): + if a == 4: # pragma: no cover + func5() + if b: + print(7) + func8() + + print(10) + """, + [1, 3, 10] + ) + self.check_coverage("""\ + class Foo: # pragma: no cover + def greet(self): + print("hello world") + """, + [] + ) + def test_excluding_method(self) -> None: self.check_coverage("""\ class Fooey: