Skip to content

Commit

Permalink
Adapt to 3.8's way of tracing decorated functions
Browse files Browse the repository at this point in the history
  • Loading branch information
nedbat committed Nov 3, 2018
1 parent d469f30 commit d8f1d92
Show file tree
Hide file tree
Showing 4 changed files with 56 additions and 14 deletions.
6 changes: 6 additions & 0 deletions coverage/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ class PYBEHAVIOR(object):
# work?
finally_jumps_back = (PYVERSION >= (3, 8))

# When a function is decorated, does the trace function get called for the
# @-line and also the def-line (new behavior in 3.8)? Or just the @-line
# (old behavior)?
trace_decorated_def = (PYVERSION >= (3, 8))


# Coverage.py specifics.

# Are we using the C-implemented trace function?
Expand Down
31 changes: 24 additions & 7 deletions coverage/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -579,9 +579,19 @@ def line_for_node(self, node):
else:
return node.lineno

def _line_decorated(self, node):
"""Compute first line number for things that can be decorated (classes and functions)."""
lineno = node.lineno
if env.PYBEHAVIOR.trace_decorated_def:
if node.decorator_list:
lineno = node.decorator_list[0].lineno
return lineno

def _line__Assign(self, node):
return self.line_for_node(node.value)

_line__ClassDef = _line_decorated

def _line__Dict(self, node):
# Python 3.5 changed how dict literals are made.
if env.PYVERSION >= (3, 5) and node.keys:
Expand All @@ -594,6 +604,8 @@ def _line__Dict(self, node):
else:
return node.lineno

_line__FunctionDef = _line_decorated

def _line__List(self, node):
if node.elts:
return self.line_for_node(node.elts[0])
Expand Down Expand Up @@ -812,10 +824,10 @@ def process_return_exits(self, exits):
# Handlers: _handle__*
#
# Each handler deals with a specific AST node type, dispatched from
# add_arcs. Each deals with a particular kind of node type, and returns
# the set of exits from that node. These functions mirror the Python
# semantics of each syntactic construct. See the docstring for add_arcs to
# understand the concept of exits from a node.
# add_arcs. Handlers return the set of exits from that node, and can
# also call self.add_arc to record arcs they find. These functions mirror
# the Python semantics of each syntactic construct. See the docstring
# for add_arcs to understand the concept of exits from a node.

@contract(returns='ArcStarts')
def _handle__Break(self, node):
Expand All @@ -827,13 +839,18 @@ def _handle__Break(self, node):
@contract(returns='ArcStarts')
def _handle_decorated(self, node):
"""Add arcs for things that can be decorated (classes and functions)."""
last = self.line_for_node(node)
main_line = last = node.lineno
if node.decorator_list:
if env.PYBEHAVIOR.trace_decorated_def:
last = None
for dec_node in node.decorator_list:
dec_start = self.line_for_node(dec_node)
if dec_start != last:
if last is not None and dec_start != last:
self.add_arc(last, dec_start)
last = dec_start
last = dec_start
if env.PYBEHAVIOR.trace_decorated_def:
self.add_arc(last, main_line)
last = main_line
# The definition line may have been missed, but we should have it
# in `self.statements`. For some constructs, `line_for_node` is
# not what we'd think of as the first line in the statement, so map
Expand Down
16 changes: 12 additions & 4 deletions tests/test_coverage.py
Original file line number Diff line number Diff line change
Expand Up @@ -1579,6 +1579,9 @@ class Py24Test(CoverageTest):
"""Tests of new syntax in Python 2.4."""

def test_function_decorators(self):
lines = [1, 2, 3, 4, 6, 8, 10, 12]
if env.PYBEHAVIOR.trace_decorated_def:
lines = sorted(lines + [9])
self.check_coverage("""\
def require_int(func):
def wrapper(arg):
Expand All @@ -1593,9 +1596,12 @@ def p1(arg):
assert p1(10) == 20
""",
[1,2,3,4,6,8,10,12], "")
lines, "")

def test_function_decorators_with_args(self):
lines = [1, 2, 3, 4, 5, 6, 8, 10, 12]
if env.PYBEHAVIOR.trace_decorated_def:
lines = sorted(lines + [9])
self.check_coverage("""\
def boost_by(extra):
def decorator(func):
Expand All @@ -1610,9 +1616,12 @@ def boosted(arg):
assert boosted(10) == 200
""",
[1,2,3,4,5,6,8,10,12], "")
lines, "")

def test_double_function_decorators(self):
lines = [1, 2, 3, 4, 5, 7, 8, 9, 10, 11, 12, 14, 15, 17, 19, 21, 22, 24, 26]
if env.PYBEHAVIOR.trace_decorated_def:
lines = sorted(lines + [16, 23])
self.check_coverage("""\
def require_int(func):
def wrapper(arg):
Expand Down Expand Up @@ -1641,8 +1650,7 @@ def boosted2(arg):
assert boosted2(10) == 200
""",
([1,2,3,4,5,7,8,9,10,11,12,14,15,17,19,21,22,24,26],
[1,2,3,4,5,7,8,9,10,11,12,14, 17,19,21, 24,26]), "")
lines, "")


class Py25Test(CoverageTest):
Expand Down
17 changes: 14 additions & 3 deletions tests/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,8 @@ def func(x=25):
return 26
""")
raw_statements = set([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, 25])
self.assertEqual(parser.raw_statements, raw_statements)
self.assertEqual(parser.statements, set([8]))

Expand Down Expand Up @@ -196,13 +198,22 @@ def foo(self):
def bar(self):
pass
""")
self.assertEqual(parser.statements, set([1, 2, 4, 8, 10]))
expected_arcs = set(self.arcz_to_arcs(".1 14 48 8. .2 2. -8A A-8"))
expected_exits = {1: 1, 2: 1, 4: 1, 8: 1, 10: 1}

if env.PYBEHAVIOR.trace_decorated_def:
expected_statements = {1, 2, 4, 5, 8, 9, 10}
expected_arcs = set(self.arcz_to_arcs(".1 14 45 58 89 9. .2 2. -8A A-8"))
expected_exits = {1: 1, 2: 1, 4: 1, 5: 1, 8: 1, 9: 1, 10: 1}
else:
expected_statements = {1, 2, 4, 8, 10}
expected_arcs = set(self.arcz_to_arcs(".1 14 48 8. .2 2. -8A A-8"))
expected_exits = {1: 1, 2: 1, 4: 1, 8: 1, 10: 1}

if env.PYVERSION >= (3, 7, 0, 'beta', 5):
# 3.7 changed how functions with only docstrings are numbered.
expected_arcs.update(set(self.arcz_to_arcs("-46 6-4")))
expected_exits.update({6: 1})

self.assertEqual(parser.statements, expected_statements)
self.assertEqual(parser.arcs(), expected_arcs)
self.assertEqual(parser.exit_counts(), expected_exits)

Expand Down

0 comments on commit d8f1d92

Please sign in to comment.