Skip to content

Commit

Permalink
Finally jumps back to exiting lines
Browse files Browse the repository at this point in the history
In Python 3.8, when a finally clause is run because a line in the try
block is exiting the block, the exiting line is visited again after the
finally block.
  • Loading branch information
nedbat committed Oct 6, 2018
1 parent cf7e871 commit 04ff188
Show file tree
Hide file tree
Showing 4 changed files with 151 additions and 45 deletions.
10 changes: 10 additions & 0 deletions coverage/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,16 @@
PY2 = PYVERSION < (3, 0)
PY3 = PYVERSION >= (3, 0)

# Python behavior
class PYBEHAVIOR(object):
"""Flags indicating this Python's behavior."""

# When a break/continue/return statement in a try block jumps to a finally
# block, does the finally block do the break/continue/return (pre-3.8), or
# does the finally jump back to the break/continue/return (3.8) to do the
# work?
finally_jumps_back = (PYVERSION >= (3, 8))

# Coverage.py specifics.

# Are we using the C-implemented trace function?
Expand Down
47 changes: 38 additions & 9 deletions coverage/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,10 @@ def __init__(self, body):
self.lineno = body[0].lineno


# TODO: some add_arcs methods here don't add arcs, they return them. Rename them.
# TODO: the cause messages have too many commas.
# TODO: Shouldn't the cause messages join with "and" instead of "or"?

class AstArcAnalyzer(object):
"""Analyze source text with an AST to find executable code paths."""

Expand Down Expand Up @@ -546,6 +550,7 @@ def analyze(self):
if code_object_handler is not None:
code_object_handler(node)

@contract(start=int, end=int)
def add_arc(self, start, end, smsg=None, emsg=None):
"""Add an arc, including message fragments to use if it is missing."""
if self.debug: # pragma: debugging
Expand Down Expand Up @@ -970,21 +975,45 @@ def _handle__Try(self, node):
final_exits = self.add_body_arcs(node.finalbody, prev_starts=final_from)

if try_block.break_from:
self.process_break_exits(
self._combine_finally_starts(try_block.break_from, final_exits)
)
if env.PYBEHAVIOR.finally_jumps_back:
for break_line in try_block.break_from:
lineno = break_line.lineno
cause = break_line.cause.format(lineno=lineno)
for final_exit in final_exits:
self.add_arc(final_exit.lineno, lineno, cause)
breaks = try_block.break_from
else:
breaks = self._combine_finally_starts(try_block.break_from, final_exits)
self.process_break_exits(breaks)

if try_block.continue_from:
self.process_continue_exits(
self._combine_finally_starts(try_block.continue_from, final_exits)
)
if env.PYBEHAVIOR.finally_jumps_back:
for continue_line in try_block.continue_from:
lineno = continue_line.lineno
cause = continue_line.cause.format(lineno=lineno)
for final_exit in final_exits:
self.add_arc(final_exit.lineno, lineno, cause)
continues = try_block.continue_from
else:
continues = self._combine_finally_starts(try_block.continue_from, final_exits)
self.process_continue_exits(continues)

if try_block.raise_from:
self.process_raise_exits(
self._combine_finally_starts(try_block.raise_from, final_exits)
)

if try_block.return_from:
self.process_return_exits(
self._combine_finally_starts(try_block.return_from, final_exits)
)
if env.PYBEHAVIOR.finally_jumps_back:
for return_line in try_block.return_from:
lineno = return_line.lineno
cause = return_line.cause.format(lineno=lineno)
for final_exit in final_exits:
self.add_arc(final_exit.lineno, lineno, cause)
returns = try_block.return_from
else:
returns = self._combine_finally_starts(try_block.return_from, final_exits)
self.process_return_exits(returns)

if exits:
# The finally clause's exits are only exits for the try block
Expand Down
70 changes: 53 additions & 17 deletions tests/test_arcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -623,6 +623,10 @@ def test_finally_in_loop(self):


def test_break_through_finally(self):
if env.PYBEHAVIOR.finally_jumps_back:
arcz = ".1 12 23 34 3D 45 56 67 68 7A 7D 8A A3 A7 BC CD D."
else:
arcz = ".1 12 23 34 3D 45 56 67 68 7A 8A A3 AD BC CD D."
self.check_coverage("""\
a, c, d, i = 1, 1, 1, 99
try:
Expand All @@ -638,11 +642,15 @@ def test_break_through_finally(self):
d = 12 # C
assert a == 5 and c == 10 and d == 1 # D
""",
arcz=".1 12 23 34 3D 45 56 67 68 7A 8A A3 AD BC CD D.",
arcz=arcz,
arcz_missing="3D BC CD",
)

def test_continue_through_finally(self):
if env.PYBEHAVIOR.finally_jumps_back:
arcz = ".1 12 23 34 3D 45 56 67 68 73 7A 8A A3 A7 BC CD D."
else:
arcz = ".1 12 23 34 3D 45 56 67 68 7A 8A A3 BC CD D."
self.check_coverage("""\
a, b, c, d, i = 1, 1, 1, 1, 99
try:
Expand All @@ -658,7 +666,7 @@ def test_continue_through_finally(self):
d = 12 # C
assert (a, b, c, d) == (5, 8, 10, 1) # D
""",
arcz=".1 12 23 34 3D 45 56 67 68 7A 8A A3 BC CD D.",
arcz=arcz,
arcz_missing="BC CD",
)

Expand Down Expand Up @@ -794,6 +802,10 @@ def test_multiple_except_clauses(self):
)

def test_return_finally(self):
if env.PYBEHAVIOR.finally_jumps_back:
arcz = ".1 12 29 9A AB BC C-1 -23 34 45 5-2 57 75 38 8-2"
else:
arcz = ".1 12 29 9A AB BC C-1 -23 34 45 57 7-2 38 8-2"
self.check_coverage("""\
a = [1]
def check_token(data):
Expand All @@ -808,10 +820,26 @@ def check_token(data):
assert check_token(True) == 5
assert a == [1, 7]
""",
arcz=".1 12 29 9A AB BC C-1 -23 34 45 57 7-2 38 8-2",
arcz=arcz,
)

def test_except_jump_finally(self):
if env.PYBEHAVIOR.finally_jumps_back:
arcz = (
".1 1Q QR RS ST TU U. "
".2 23 34 45 56 4O 6L "
"78 89 9A AL LA AO 8B BC CD DL LD D4 BE EF FG GL LG G. EH HI IJ JL HL "
"L4 LM "
"MN NO O."
)
else:
arcz = (
".1 1Q QR RS ST TU U. "
".2 23 34 45 56 4O 6L "
"78 89 9A AL 8B BC CD DL BE EF FG GL EH HI IJ JL HL "
"LO L4 L. LM "
"MN NO O."
)
self.check_coverage("""\
def func(x):
a = f = g = 2
Expand Down Expand Up @@ -842,18 +870,30 @@ def func(x):
assert func('continue') == (12, 21, 2, 3) # R
assert func('return') == (15, 2, 2, 0) # S
assert func('raise') == (18, 21, 23, 0) # T
assert func('other') == (2, 21, 2, 3) # U 30
""",
arcz=
".1 1Q QR RS ST T. "
".2 23 34 45 56 4O 6L "
"78 89 9A AL 8B BC CD DL BE EF FG GL EH HI IJ JL HL "
"LO L4 L. LM "
"MN NO O.",
arcz_missing="6L HL",
arcz=arcz,
arcz_missing="6L",
arcz_unpredicted="67",
)

def test_else_jump_finally(self):
if env.PYBEHAVIOR.finally_jumps_back:
arcz = (
".1 1S ST TU UV VW W. "
".2 23 34 45 56 6A 78 8N 4Q "
"AB BC CN NC CQ AD DE EF FN NF F4 DG GH HI IN NI I. GJ JK KL LN JN "
"N4 NO "
"OP PQ Q."
)
else:
arcz = (
".1 1S ST TU UV VW W. "
".2 23 34 45 56 6A 78 8N 4Q "
"AB BC CN AD DE EF FN DG GH HI IN GJ JK KL LN JN "
"N4 NQ N. NO "
"OP PQ Q."
)
self.check_coverage("""\
def func(x):
a = f = g = 2
Expand Down Expand Up @@ -886,14 +926,10 @@ def func(x):
assert func('continue') == (14, 23, 2, 3) # T
assert func('return') == (17, 2, 2, 0) # U
assert func('raise') == (20, 23, 25, 0) # V
assert func('other') == (2, 23, 2, 3) # W 32
""",
arcz=
".1 1S ST TU UV V. "
".2 23 34 45 56 6A 78 8N 4Q "
"AB BC CN AD DE EF FN DG GH HI IN GJ JK KL LN JN "
"NQ N4 N. NO "
"OP PQ Q.",
arcz_missing="78 8N JN",
arcz=arcz,
arcz_missing="78 8N",
arcz_unpredicted="",
)

Expand Down
69 changes: 50 additions & 19 deletions tests/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -326,26 +326,57 @@ def function():
this_thing(16)
that_thing(17)
""")
self.assertEqual(
parser.missing_arc_description(16, 17),
"line 16 didn't jump to line 17, because the break on line 5 wasn't executed"
)
self.assertEqual(
parser.missing_arc_description(16, 2),
"line 16 didn't jump to line 2, "
"because the continue on line 8 wasn't executed"
if env.PYBEHAVIOR.finally_jumps_back:
self.assertEqual(
parser.missing_arc_description(16, 5),
"line 16 didn't jump to line 5, because the break on line 5 wasn't executed"
)
self.assertEqual(
parser.missing_arc_description(5, 17),
"line 5 didn't jump to line 17, because the break on line 5 wasn't executed"
)
self.assertEqual(
parser.missing_arc_description(16, 8),
"line 16 didn't jump to line 8, because the continue on line 8 wasn't executed"
)
self.assertEqual(
parser.missing_arc_description(8, 2),
"line 8 didn't jump to line 2, because the continue on line 8 wasn't executed"
)
self.assertEqual(
parser.missing_arc_description(16, 12),
"line 16 didn't jump to line 12, because the return on line 12 wasn't executed"
)
self.assertEqual(
parser.missing_arc_description(12, -1),
"line 12 didn't return from function 'function', "
"because the return on line 12 wasn't executed"
)
self.assertEqual(
parser.missing_arc_description(16, -1),
"line 16 didn't except from function 'function', "
"because the raise on line 14 wasn't executed"
)
else:
self.assertEqual(
parser.missing_arc_description(16, 17),
"line 16 didn't jump to line 17, because the break on line 5 wasn't executed"
)
self.assertEqual(
parser.missing_arc_description(16, 2),
"line 16 didn't jump to line 2, "
"because the continue on line 8 wasn't executed"
" or "
"the continue on line 10 wasn't executed"
)
self.assertEqual(
parser.missing_arc_description(16, -1),
"line 16 didn't except from function 'function', "
"because the raise on line 14 wasn't executed"
" or "
"the continue on line 10 wasn't executed"
)
self.assertEqual(
parser.missing_arc_description(16, -1),
"line 16 didn't except from function 'function', "
"because the raise on line 14 wasn't executed"
" or "
"line 16 didn't return from function 'function', "
"because the return on line 12 wasn't executed"
)

"line 16 didn't return from function 'function', "
"because the return on line 12 wasn't executed"
)
def test_missing_arc_descriptions_bug460(self):
parser = self.parse_text(u"""\
x = 1
Expand Down

0 comments on commit 04ff188

Please sign in to comment.