summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNed Batchelder <ned@nedbatchelder.com>2021-11-10 21:53:06 -0500
committerNed Batchelder <ned@nedbatchelder.com>2021-11-10 21:53:06 -0500
commit0387bbaa5e9ab2ad834311249802695ebb0c34ca (patch)
tree81eba7ac0b1fe21c4ad3230efcc9a97fe920e606
parentd1d60a72cd60b2472d02cdbbcb487e31f25fe32a (diff)
downloadpython-coveragepy-git-0387bbaa5e9ab2ad834311249802695ebb0c34ca.tar.gz
fix: colons in decorators shouldn't stop an exclusion
-rw-r--r--CHANGES.rst3
-rw-r--r--coverage/parser.py9
-rw-r--r--tests/test_parser.py22
3 files changed, 30 insertions, 4 deletions
diff --git a/CHANGES.rst b/CHANGES.rst
index 15c81aaf..0b6e2490 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -22,7 +22,8 @@ This list is detailed and covers changes in each pre-release version.
Unreleased
----------
-Nothing yet.
+- Fix: A colon in a decorator expression would cause an exclusion to end too
+ early, preventing the exclusion of the decorated function. This is now fixed.
.. _changes_612:
diff --git a/coverage/parser.py b/coverage/parser.py
index ec788c06..b47fd12e 100644
--- a/coverage/parser.py
+++ b/coverage/parser.py
@@ -67,7 +67,7 @@ class PythonParser:
# The raw line numbers of excluded lines of code, as marked by pragmas.
self.raw_excluded = set()
- # The line numbers of class and function definitions.
+ # The line numbers of class definitions.
self.raw_classdefs = set()
# The line numbers of docstring lines.
@@ -120,6 +120,7 @@ class PythonParser:
first_line = None
empty = True
first_on_line = True
+ nesting = 0
tokgen = generate_tokens(self.text)
for toktype, ttext, (slineno, _), (elineno, _), ltext in tokgen:
@@ -139,7 +140,7 @@ class PythonParser:
# lines with the 'class' keyword.
self.raw_classdefs.add(slineno)
elif toktype == token.OP:
- if ttext == ':':
+ if ttext == ':' and nesting == 0:
should_exclude = (elineno in self.raw_excluded) or excluding_decorators
if not excluding and should_exclude:
# Start excluding a suite. We trigger off of the colon
@@ -155,6 +156,10 @@ class PythonParser:
excluding_decorators = True
if excluding_decorators:
self.raw_excluded.add(elineno)
+ elif ttext in "([{":
+ nesting += 1
+ elif ttext in ")]}":
+ nesting -= 1
elif toktype == token.STRING and prev_toktype == token.INDENT:
# Strings that are first on an indented line are docstrings.
# (a trick from trace.py in the stdlib.) This works for
diff --git a/tests/test_parser.py b/tests/test_parser.py
index d5f43197..303f2b55 100644
--- a/tests/test_parser.py
+++ b/tests/test_parser.py
@@ -174,10 +174,30 @@ class PythonParserTest(CoverageTest):
""")
raw_statements = {3, 4, 5, 6, 8, 9, 10, 13, 15, 16, 17, 20, 22, 23, 25, 26}
if env.PYBEHAVIOR.trace_decorated_def:
- raw_statements.update([11, 19])
+ raw_statements.update({11, 19})
assert parser.raw_statements == raw_statements
assert parser.statements == {8}
+ def test_decorator_pragmas_with_colons(self):
+ # A colon in a decorator expression would confuse the parser,
+ # ending the exclusion of the decorated function.
+ parser = self.parse_source("""\
+ @decorate(X) # nocover
+ @decorate("Hello"[2])
+ def f():
+ x = 4
+
+ @decorate(X) # nocover
+ @decorate("Hello"[:7])
+ def g():
+ x = 9
+ """)
+ raw_statements = {1, 2, 4, 6, 7, 9}
+ if env.PYBEHAVIOR.trace_decorated_def:
+ raw_statements.update({3, 8})
+ assert parser.raw_statements == raw_statements
+ assert parser.statements == set()
+
def test_class_decorator_pragmas(self):
parser = self.parse_source("""\
class Foo(object):