Skip to content

Commit 04ff188

Browse files
committed
Finally jumps back to exiting lines
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.
1 parent cf7e871 commit 04ff188

File tree

4 files changed

+151
-45
lines changed

4 files changed

+151
-45
lines changed

coverage/env.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,16 @@
2424
PY2 = PYVERSION < (3, 0)
2525
PY3 = PYVERSION >= (3, 0)
2626

27+
# Python behavior
28+
class PYBEHAVIOR(object):
29+
"""Flags indicating this Python's behavior."""
30+
31+
# When a break/continue/return statement in a try block jumps to a finally
32+
# block, does the finally block do the break/continue/return (pre-3.8), or
33+
# does the finally jump back to the break/continue/return (3.8) to do the
34+
# work?
35+
finally_jumps_back = (PYVERSION >= (3, 8))
36+
2737
# Coverage.py specifics.
2838

2939
# Are we using the C-implemented trace function?

coverage/parser.py

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -505,6 +505,10 @@ def __init__(self, body):
505505
self.lineno = body[0].lineno
506506

507507

508+
# TODO: some add_arcs methods here don't add arcs, they return them. Rename them.
509+
# TODO: the cause messages have too many commas.
510+
# TODO: Shouldn't the cause messages join with "and" instead of "or"?
511+
508512
class AstArcAnalyzer(object):
509513
"""Analyze source text with an AST to find executable code paths."""
510514

@@ -546,6 +550,7 @@ def analyze(self):
546550
if code_object_handler is not None:
547551
code_object_handler(node)
548552

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

972977
if try_block.break_from:
973-
self.process_break_exits(
974-
self._combine_finally_starts(try_block.break_from, final_exits)
975-
)
978+
if env.PYBEHAVIOR.finally_jumps_back:
979+
for break_line in try_block.break_from:
980+
lineno = break_line.lineno
981+
cause = break_line.cause.format(lineno=lineno)
982+
for final_exit in final_exits:
983+
self.add_arc(final_exit.lineno, lineno, cause)
984+
breaks = try_block.break_from
985+
else:
986+
breaks = self._combine_finally_starts(try_block.break_from, final_exits)
987+
self.process_break_exits(breaks)
988+
976989
if try_block.continue_from:
977-
self.process_continue_exits(
978-
self._combine_finally_starts(try_block.continue_from, final_exits)
979-
)
990+
if env.PYBEHAVIOR.finally_jumps_back:
991+
for continue_line in try_block.continue_from:
992+
lineno = continue_line.lineno
993+
cause = continue_line.cause.format(lineno=lineno)
994+
for final_exit in final_exits:
995+
self.add_arc(final_exit.lineno, lineno, cause)
996+
continues = try_block.continue_from
997+
else:
998+
continues = self._combine_finally_starts(try_block.continue_from, final_exits)
999+
self.process_continue_exits(continues)
1000+
9801001
if try_block.raise_from:
9811002
self.process_raise_exits(
9821003
self._combine_finally_starts(try_block.raise_from, final_exits)
9831004
)
1005+
9841006
if try_block.return_from:
985-
self.process_return_exits(
986-
self._combine_finally_starts(try_block.return_from, final_exits)
987-
)
1007+
if env.PYBEHAVIOR.finally_jumps_back:
1008+
for return_line in try_block.return_from:
1009+
lineno = return_line.lineno
1010+
cause = return_line.cause.format(lineno=lineno)
1011+
for final_exit in final_exits:
1012+
self.add_arc(final_exit.lineno, lineno, cause)
1013+
returns = try_block.return_from
1014+
else:
1015+
returns = self._combine_finally_starts(try_block.return_from, final_exits)
1016+
self.process_return_exits(returns)
9881017

9891018
if exits:
9901019
# The finally clause's exits are only exits for the try block

tests/test_arcs.py

Lines changed: 53 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -623,6 +623,10 @@ def test_finally_in_loop(self):
623623

624624

625625
def test_break_through_finally(self):
626+
if env.PYBEHAVIOR.finally_jumps_back:
627+
arcz = ".1 12 23 34 3D 45 56 67 68 7A 7D 8A A3 A7 BC CD D."
628+
else:
629+
arcz = ".1 12 23 34 3D 45 56 67 68 7A 8A A3 AD BC CD D."
626630
self.check_coverage("""\
627631
a, c, d, i = 1, 1, 1, 99
628632
try:
@@ -638,11 +642,15 @@ def test_break_through_finally(self):
638642
d = 12 # C
639643
assert a == 5 and c == 10 and d == 1 # D
640644
""",
641-
arcz=".1 12 23 34 3D 45 56 67 68 7A 8A A3 AD BC CD D.",
645+
arcz=arcz,
642646
arcz_missing="3D BC CD",
643647
)
644648

645649
def test_continue_through_finally(self):
650+
if env.PYBEHAVIOR.finally_jumps_back:
651+
arcz = ".1 12 23 34 3D 45 56 67 68 73 7A 8A A3 A7 BC CD D."
652+
else:
653+
arcz = ".1 12 23 34 3D 45 56 67 68 7A 8A A3 BC CD D."
646654
self.check_coverage("""\
647655
a, b, c, d, i = 1, 1, 1, 1, 99
648656
try:
@@ -658,7 +666,7 @@ def test_continue_through_finally(self):
658666
d = 12 # C
659667
assert (a, b, c, d) == (5, 8, 10, 1) # D
660668
""",
661-
arcz=".1 12 23 34 3D 45 56 67 68 7A 8A A3 BC CD D.",
669+
arcz=arcz,
662670
arcz_missing="BC CD",
663671
)
664672

@@ -794,6 +802,10 @@ def test_multiple_except_clauses(self):
794802
)
795803

796804
def test_return_finally(self):
805+
if env.PYBEHAVIOR.finally_jumps_back:
806+
arcz = ".1 12 29 9A AB BC C-1 -23 34 45 5-2 57 75 38 8-2"
807+
else:
808+
arcz = ".1 12 29 9A AB BC C-1 -23 34 45 57 7-2 38 8-2"
797809
self.check_coverage("""\
798810
a = [1]
799811
def check_token(data):
@@ -808,10 +820,26 @@ def check_token(data):
808820
assert check_token(True) == 5
809821
assert a == [1, 7]
810822
""",
811-
arcz=".1 12 29 9A AB BC C-1 -23 34 45 57 7-2 38 8-2",
823+
arcz=arcz,
812824
)
813825

814826
def test_except_jump_finally(self):
827+
if env.PYBEHAVIOR.finally_jumps_back:
828+
arcz = (
829+
".1 1Q QR RS ST TU U. "
830+
".2 23 34 45 56 4O 6L "
831+
"78 89 9A AL LA AO 8B BC CD DL LD D4 BE EF FG GL LG G. EH HI IJ JL HL "
832+
"L4 LM "
833+
"MN NO O."
834+
)
835+
else:
836+
arcz = (
837+
".1 1Q QR RS ST TU U. "
838+
".2 23 34 45 56 4O 6L "
839+
"78 89 9A AL 8B BC CD DL BE EF FG GL EH HI IJ JL HL "
840+
"LO L4 L. LM "
841+
"MN NO O."
842+
)
815843
self.check_coverage("""\
816844
def func(x):
817845
a = f = g = 2
@@ -842,18 +870,30 @@ def func(x):
842870
assert func('continue') == (12, 21, 2, 3) # R
843871
assert func('return') == (15, 2, 2, 0) # S
844872
assert func('raise') == (18, 21, 23, 0) # T
873+
assert func('other') == (2, 21, 2, 3) # U 30
845874
""",
846-
arcz=
847-
".1 1Q QR RS ST T. "
848-
".2 23 34 45 56 4O 6L "
849-
"78 89 9A AL 8B BC CD DL BE EF FG GL EH HI IJ JL HL "
850-
"LO L4 L. LM "
851-
"MN NO O.",
852-
arcz_missing="6L HL",
875+
arcz=arcz,
876+
arcz_missing="6L",
853877
arcz_unpredicted="67",
854878
)
855879

856880
def test_else_jump_finally(self):
881+
if env.PYBEHAVIOR.finally_jumps_back:
882+
arcz = (
883+
".1 1S ST TU UV VW W. "
884+
".2 23 34 45 56 6A 78 8N 4Q "
885+
"AB BC CN NC CQ AD DE EF FN NF F4 DG GH HI IN NI I. GJ JK KL LN JN "
886+
"N4 NO "
887+
"OP PQ Q."
888+
)
889+
else:
890+
arcz = (
891+
".1 1S ST TU UV VW W. "
892+
".2 23 34 45 56 6A 78 8N 4Q "
893+
"AB BC CN AD DE EF FN DG GH HI IN GJ JK KL LN JN "
894+
"N4 NQ N. NO "
895+
"OP PQ Q."
896+
)
857897
self.check_coverage("""\
858898
def func(x):
859899
a = f = g = 2
@@ -886,14 +926,10 @@ def func(x):
886926
assert func('continue') == (14, 23, 2, 3) # T
887927
assert func('return') == (17, 2, 2, 0) # U
888928
assert func('raise') == (20, 23, 25, 0) # V
929+
assert func('other') == (2, 23, 2, 3) # W 32
889930
""",
890-
arcz=
891-
".1 1S ST TU UV V. "
892-
".2 23 34 45 56 6A 78 8N 4Q "
893-
"AB BC CN AD DE EF FN DG GH HI IN GJ JK KL LN JN "
894-
"NQ N4 N. NO "
895-
"OP PQ Q.",
896-
arcz_missing="78 8N JN",
931+
arcz=arcz,
932+
arcz_missing="78 8N",
897933
arcz_unpredicted="",
898934
)
899935

tests/test_parser.py

Lines changed: 50 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -326,26 +326,57 @@ def function():
326326
this_thing(16)
327327
that_thing(17)
328328
""")
329-
self.assertEqual(
330-
parser.missing_arc_description(16, 17),
331-
"line 16 didn't jump to line 17, because the break on line 5 wasn't executed"
332-
)
333-
self.assertEqual(
334-
parser.missing_arc_description(16, 2),
335-
"line 16 didn't jump to line 2, "
336-
"because the continue on line 8 wasn't executed"
329+
if env.PYBEHAVIOR.finally_jumps_back:
330+
self.assertEqual(
331+
parser.missing_arc_description(16, 5),
332+
"line 16 didn't jump to line 5, because the break on line 5 wasn't executed"
333+
)
334+
self.assertEqual(
335+
parser.missing_arc_description(5, 17),
336+
"line 5 didn't jump to line 17, because the break on line 5 wasn't executed"
337+
)
338+
self.assertEqual(
339+
parser.missing_arc_description(16, 8),
340+
"line 16 didn't jump to line 8, because the continue on line 8 wasn't executed"
341+
)
342+
self.assertEqual(
343+
parser.missing_arc_description(8, 2),
344+
"line 8 didn't jump to line 2, because the continue on line 8 wasn't executed"
345+
)
346+
self.assertEqual(
347+
parser.missing_arc_description(16, 12),
348+
"line 16 didn't jump to line 12, because the return on line 12 wasn't executed"
349+
)
350+
self.assertEqual(
351+
parser.missing_arc_description(12, -1),
352+
"line 12 didn't return from function 'function', "
353+
"because the return on line 12 wasn't executed"
354+
)
355+
self.assertEqual(
356+
parser.missing_arc_description(16, -1),
357+
"line 16 didn't except from function 'function', "
358+
"because the raise on line 14 wasn't executed"
359+
)
360+
else:
361+
self.assertEqual(
362+
parser.missing_arc_description(16, 17),
363+
"line 16 didn't jump to line 17, because the break on line 5 wasn't executed"
364+
)
365+
self.assertEqual(
366+
parser.missing_arc_description(16, 2),
367+
"line 16 didn't jump to line 2, "
368+
"because the continue on line 8 wasn't executed"
369+
" or "
370+
"the continue on line 10 wasn't executed"
371+
)
372+
self.assertEqual(
373+
parser.missing_arc_description(16, -1),
374+
"line 16 didn't except from function 'function', "
375+
"because the raise on line 14 wasn't executed"
337376
" or "
338-
"the continue on line 10 wasn't executed"
339-
)
340-
self.assertEqual(
341-
parser.missing_arc_description(16, -1),
342-
"line 16 didn't except from function 'function', "
343-
"because the raise on line 14 wasn't executed"
344-
" or "
345-
"line 16 didn't return from function 'function', "
346-
"because the return on line 12 wasn't executed"
347-
)
348-
377+
"line 16 didn't return from function 'function', "
378+
"because the return on line 12 wasn't executed"
379+
)
349380
def test_missing_arc_descriptions_bug460(self):
350381
parser = self.parse_text(u"""\
351382
x = 1

0 commit comments

Comments
 (0)