Skip to content

Commit f9a7d41

Browse files
authored
gh-96092: Fix traceback.walk_stack(None) skipping too many frames (#129330)
As it says in its documentation, walk_stack was meant to just follow `f.f_back` like other functions in the traceback module. Instead it was previously doing `f.f_back.f_back` and then this changed to `f_back.f_back.f_back.f_back' in Python 3.11 breaking its behavior for external users. This happened because the walk_stack function never really had any good direct tests and its only consumer in the traceback module was `extract_stack` which passed the result into `StackSummary.extract`. As a generator, it was previously capturing the state of the stack when it was first iterated over, rather than the stack when `walk_stack` was called. Meaning when called inside the two method deep `extract` and `extract_stack` calls, two `f_back`s were needed. When 3.11 modified the sequence of calls in `extract`, two more `f_back`s were needed to make the tests happy. This changes the generator to capture the stack when `walk_stack` is called, rather than when it is first iterated over. Since this is technically a breaking change in behavior, there is a versionchanged to the documentation. In practice, this is unlikely to break anyone, you would have been needing to store the result of `walk_stack` and expecting it to change.
1 parent 6fb5138 commit f9a7d41

File tree

4 files changed

+25
-6
lines changed

4 files changed

+25
-6
lines changed

Doc/library/traceback.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,11 @@ Module-Level Functions
257257

258258
.. versionadded:: 3.5
259259

260+
.. versionchanged:: 3.14
261+
This function previously returned a generator that would walk the stack
262+
when first iterated over. The generator returned now is the state of the
263+
stack when ``walk_stack`` is called.
264+
260265
.. function:: walk_tb(tb)
261266

262267
Walk a traceback following :attr:`~traceback.tb_next` yielding the frame and

Lib/test/test_traceback.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3229,11 +3229,17 @@ class TestStack(unittest.TestCase):
32293229
def test_walk_stack(self):
32303230
def deeper():
32313231
return list(traceback.walk_stack(None))
3232-
s1 = list(traceback.walk_stack(None))
3233-
s2 = deeper()
3232+
s1, s2 = list(traceback.walk_stack(None)), deeper()
32343233
self.assertEqual(len(s2) - len(s1), 1)
32353234
self.assertEqual(s2[1:], s1)
32363235

3236+
def test_walk_innermost_frame(self):
3237+
def inner():
3238+
return list(traceback.walk_stack(None))
3239+
frames = inner()
3240+
innermost_frame, _ = frames[0]
3241+
self.assertEqual(innermost_frame.f_code.co_name, "inner")
3242+
32373243
def test_walk_tb(self):
32383244
try:
32393245
1/0

Lib/traceback.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -380,10 +380,14 @@ def walk_stack(f):
380380
current stack is used. Usually used with StackSummary.extract.
381381
"""
382382
if f is None:
383-
f = sys._getframe().f_back.f_back.f_back.f_back
384-
while f is not None:
385-
yield f, f.f_lineno
386-
f = f.f_back
383+
f = sys._getframe().f_back
384+
385+
def walk_stack_generator(frame):
386+
while frame is not None:
387+
yield frame, frame.f_lineno
388+
frame = frame.f_back
389+
390+
return walk_stack_generator(f)
387391

388392

389393
def walk_tb(tb):
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Fix bug in :func:`traceback.walk_stack` called with None where it was skipping
2+
more frames than in prior versions. This bug fix also changes walk_stack to
3+
walk the stack in the frame where it was called rather than where it first gets
4+
used.

0 commit comments

Comments
 (0)