Skip to content

Commit 73e58fa

Browse files
committed
refactor: clarify the code that fixes with-statement exits
1 parent e16c9cc commit 73e58fa

File tree

2 files changed

+95
-17
lines changed

2 files changed

+95
-17
lines changed

coverage/env.py

+57-1
Original file line numberDiff line numberDiff line change
@@ -79,10 +79,66 @@ class PYBEHAVIOR:
7979
keep_constant_test = pep626
8080

8181
# When leaving a with-block, do we visit the with-line again for the exit?
82+
# For example, wwith.py:
83+
#
84+
# with open("/tmp/test", "w") as f1:
85+
# a = 2
86+
# with open("/tmp/test2", "w") as f3:
87+
# print(4)
88+
#
89+
# % python3.9 -m trace -t wwith.py | grep wwith
90+
# --- modulename: wwith, funcname: <module>
91+
# wwith.py(1): with open("/tmp/test", "w") as f1:
92+
# wwith.py(2): a = 2
93+
# wwith.py(3): with open("/tmp/test2", "w") as f3:
94+
# wwith.py(4): print(4)
95+
#
96+
# % python3.10 -m trace -t wwith.py | grep wwith
97+
# --- modulename: wwith, funcname: <module>
98+
# wwith.py(1): with open("/tmp/test", "w") as f1:
99+
# wwith.py(2): a = 2
100+
# wwith.py(3): with open("/tmp/test2", "w") as f3:
101+
# wwith.py(4): print(4)
102+
# wwith.py(3): with open("/tmp/test2", "w") as f3:
103+
# wwith.py(1): with open("/tmp/test", "w") as f1:
104+
#
82105
exit_through_with = (PYVERSION >= (3, 10, 0, "beta"))
83106

84107
# When leaving a with-block, do we visit the with-line exactly,
85-
# or the inner-most context manager?
108+
# or the context managers in inner-out order?
109+
#
110+
# mwith.py:
111+
# with (
112+
# open("/tmp/one", "w") as f2,
113+
# open("/tmp/two", "w") as f3,
114+
# open("/tmp/three", "w") as f4,
115+
# ):
116+
# print("hello 6")
117+
#
118+
# % python3.11 -m trace -t mwith.py | grep mwith
119+
# --- modulename: mwith, funcname: <module>
120+
# mwith.py(2): open("/tmp/one", "w") as f2,
121+
# mwith.py(1): with (
122+
# mwith.py(2): open("/tmp/one", "w") as f2,
123+
# mwith.py(3): open("/tmp/two", "w") as f3,
124+
# mwith.py(1): with (
125+
# mwith.py(3): open("/tmp/two", "w") as f3,
126+
# mwith.py(4): open("/tmp/three", "w") as f4,
127+
# mwith.py(1): with (
128+
# mwith.py(4): open("/tmp/three", "w") as f4,
129+
# mwith.py(6): print("hello 6")
130+
# mwith.py(1): with (
131+
#
132+
# % python3.12 -m trace -t mwith.py | grep mwith
133+
# --- modulename: mwith, funcname: <module>
134+
# mwith.py(2): open("/tmp/one", "w") as f2,
135+
# mwith.py(3): open("/tmp/two", "w") as f3,
136+
# mwith.py(4): open("/tmp/three", "w") as f4,
137+
# mwith.py(6): print("hello 6")
138+
# mwith.py(4): open("/tmp/three", "w") as f4,
139+
# mwith.py(3): open("/tmp/two", "w") as f3,
140+
# mwith.py(2): open("/tmp/one", "w") as f2,
141+
86142
exit_with_through_ctxmgr = (PYVERSION >= (3, 12, 6))
87143

88144
# Match-case construct.

coverage/parser.py

+38-16
Original file line numberDiff line numberDiff line change
@@ -300,10 +300,14 @@ def _analyze_ast(self) -> None:
300300
assert self._ast_root is not None
301301
aaa = AstArcAnalyzer(self.filename, self._ast_root, self.raw_statements, self._multiline)
302302
aaa.analyze()
303-
self._with_jump_fixers = aaa.with_jump_fixers()
303+
arcs = aaa.arcs
304+
if env.PYBEHAVIOR.exit_through_with:
305+
self._with_jump_fixers = aaa.with_jump_fixers()
306+
if self._with_jump_fixers:
307+
arcs = self.fix_with_jumps(arcs)
304308

305309
self._all_arcs = set()
306-
for l1, l2 in self.fix_with_jumps(aaa.arcs):
310+
for l1, l2 in arcs:
307311
fl1 = self.first_line(l1)
308312
fl2 = self.first_line(l2)
309313
if fl1 != fl2:
@@ -312,20 +316,41 @@ def _analyze_ast(self) -> None:
312316
self._missing_arc_fragments = aaa.missing_arc_fragments
313317

314318
def fix_with_jumps(self, arcs: Iterable[TArc]) -> set[TArc]:
315-
"""Adjust arcs to fix jumps leaving `with` statements."""
319+
"""Adjust arcs to fix jumps leaving `with` statements.
320+
321+
Consider this code:
322+
323+
with open("/tmp/test", "w") as f1:
324+
a = 2
325+
b = 3
326+
print(4)
327+
328+
In 3.10+, we get traces for lines 1, 2, 3, 1, 4. But we want to present
329+
it to the user as if it had been 1, 2, 3, 4. The arc 3->1 should be
330+
replaced with 3->4, and 1->4 should be removed.
331+
332+
For this code, the fixers dict is {(3, 1): ((1, 4), (3, 4))}. The key
333+
is the actual measured arc from the end of the with block back to the
334+
start of the with-statement. The values are start_next (the with
335+
statement to the next statement after the with), and end_next (the end
336+
of the with-statement to the next statement after the with).
337+
338+
With nested with-statements, we have to trace through a few levels to
339+
correct a longer chain of arcs.
340+
341+
"""
316342
to_remove = set()
317343
to_add = set()
318344
for arc in arcs:
319345
if arc in self._with_jump_fixers:
320-
start = arc[0]
346+
end0 = arc[0]
321347
to_remove.add(arc)
322-
start_next, prev_next = self._with_jump_fixers[arc]
348+
start_next, end_next = self._with_jump_fixers[arc]
323349
while start_next in self._with_jump_fixers:
324350
to_remove.add(start_next)
325-
start_next, prev_next = self._with_jump_fixers[start_next]
326-
to_remove.add(prev_next)
327-
to_add.add((start, prev_next[1]))
328-
to_remove.add(arc)
351+
start_next, end_next = self._with_jump_fixers[start_next]
352+
to_remove.add(end_next)
353+
to_add.add((end0, end_next[1]))
329354
to_remove.add(start_next)
330355
arcs = (set(arcs) | to_add) - to_remove
331356
return arcs
@@ -700,15 +725,12 @@ def analyze(self) -> None:
700725
def with_jump_fixers(self) -> dict[TArc, tuple[TArc, TArc]]:
701726
"""Get a dict with data for fixing jumps out of with statements.
702727
703-
Returns a dict. The keys are arcs leaving a with statement by jumping
728+
Returns a dict. The keys are arcs leaving a with-statement by jumping
704729
back to its start. The values are pairs: first, the arc from the start
705730
to the next statement, then the arc that exits the with without going
706731
to the start.
707732
708733
"""
709-
if not env.PYBEHAVIOR.exit_through_with:
710-
return {}
711-
712734
fixers = {}
713735
with_nexts = {
714736
arc
@@ -721,9 +743,9 @@ def with_jump_fixers(self) -> dict[TArc, tuple[TArc, TArc]]:
721743
continue
722744
assert len(nexts) == 1, f"Expected one arc, got {nexts} with {start = }"
723745
nxt = nexts.pop()
724-
prvs = {arc[0] for arc in self.with_exits if arc[1] == start}
725-
for prv in prvs:
726-
fixers[(prv, start)] = ((start, nxt), (prv, nxt))
746+
ends = {arc[0] for arc in self.with_exits if arc[1] == start}
747+
for end in ends:
748+
fixers[(end, start)] = ((start, nxt), (end, nxt))
727749
return fixers
728750

729751
# Code object dispatchers: _code_object__*

0 commit comments

Comments
 (0)