From bd9d061223eabbc5dabbb0636279b9f86c02e7da Mon Sep 17 00:00:00 2001 From: Ammar Askar Date: Sun, 26 Jan 2025 19:54:26 -0500 Subject: [PATCH] gh-96092: Fix traceback.walk_stack(None) skipping too many frames 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. --- Doc/library/traceback.rst | 5 +++++ Lib/test/test_traceback.py | 10 ++++++++-- Lib/traceback.py | 12 ++++++++---- .../2025-01-26-19-35-06.gh-issue-96092.mMg3gL.rst | 4 ++++ 4 files changed, 25 insertions(+), 6 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2025-01-26-19-35-06.gh-issue-96092.mMg3gL.rst diff --git a/Doc/library/traceback.rst b/Doc/library/traceback.rst index b0ee3fc56ad735..7e05144bfb34cb 100644 --- a/Doc/library/traceback.rst +++ b/Doc/library/traceback.rst @@ -257,6 +257,11 @@ Module-Level Functions .. versionadded:: 3.5 + .. versionchanged:: 3.14 + This function previously returned a generator that would walk the stack + when first iterated over. The generator returned now is the state of the + stack when ``walk_stack`` is called. + .. function:: walk_tb(tb) Walk a traceback following :attr:`~traceback.tb_next` yielding the frame and diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index 89980ae6f8573a..c2b115b53889d3 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -3229,11 +3229,17 @@ class TestStack(unittest.TestCase): def test_walk_stack(self): def deeper(): return list(traceback.walk_stack(None)) - s1 = list(traceback.walk_stack(None)) - s2 = deeper() + s1, s2 = list(traceback.walk_stack(None)), deeper() self.assertEqual(len(s2) - len(s1), 1) self.assertEqual(s2[1:], s1) + def test_walk_innermost_frame(self): + def inner(): + return list(traceback.walk_stack(None)) + frames = inner() + innermost_frame, _ = frames[0] + self.assertEqual(innermost_frame.f_code.co_name, "inner") + def test_walk_tb(self): try: 1/0 diff --git a/Lib/traceback.py b/Lib/traceback.py index 31c73efcef5a52..2b402dd4cc2401 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -380,10 +380,14 @@ def walk_stack(f): current stack is used. Usually used with StackSummary.extract. """ if f is None: - f = sys._getframe().f_back.f_back.f_back.f_back - while f is not None: - yield f, f.f_lineno - f = f.f_back + f = sys._getframe().f_back + + def walk_stack_generator(frame): + while frame is not None: + yield frame, frame.f_lineno + frame = frame.f_back + + return walk_stack_generator(f) def walk_tb(tb): diff --git a/Misc/NEWS.d/next/Library/2025-01-26-19-35-06.gh-issue-96092.mMg3gL.rst b/Misc/NEWS.d/next/Library/2025-01-26-19-35-06.gh-issue-96092.mMg3gL.rst new file mode 100644 index 00000000000000..623f7d278c6675 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-01-26-19-35-06.gh-issue-96092.mMg3gL.rst @@ -0,0 +1,4 @@ +Fix bug in :func:`traceback.walk_stack` called with None where it was skipping +more frames than in prior versions. This bug fix also changes walk_stack to +walk the stack in the frame where it was called rather than where it first gets +used.