Skip to content

Commit 135ed88

Browse files
authored
Improve canonicalize_exception_traceback for RecursionError (#1455)
1 parent df8f36d commit 135ed88

File tree

2 files changed

+75
-1
lines changed

2 files changed

+75
-1
lines changed

logfire/_internal/utils.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -449,14 +449,25 @@ def canonicalize_exception_traceback(exc: BaseException, seen: set[int] | None =
449449
try:
450450
exc_type = type(exc)
451451
parts = [f'\n{exc_type.__module__}.{exc_type.__qualname__}\n----']
452+
num_repeats = 0
452453
if exc.__traceback__:
453454
visited: set[str] = set()
454455
for frame, lineno in traceback.walk_tb(exc.__traceback__):
455456
filename = frame.f_code.co_filename
456457
source_line = linecache.getline(filename, lineno, frame.f_globals).strip()
457458
module = frame.f_globals.get('__name__', filename)
458459
frame_summary = f'{module}.{frame.f_code.co_name}\n {source_line}'
459-
if frame_summary not in visited: # ignore repeated frames
460+
if frame_summary in visited:
461+
num_repeats += 1
462+
if num_repeats >= 100 and isinstance(exc, RecursionError):
463+
# The last few frames of a RecursionError traceback are often *not* the recursive function(s)
464+
# being called repeatedly (which are already deduped here) but instead some other function(s)
465+
# called normally which happen to use up the last bit of the recursion limit.
466+
# These can easily vary between runs and we don't want to pay attention to them,
467+
# the real problem is the recursion itself.
468+
parts.append('\n<recursion detected>')
469+
break
470+
else: # skip repeated frames
460471
visited.add(frame_summary)
461472
parts.append(frame_summary)
462473
seen = seen or set()

tests/test_canonicalize_exception.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import sys
2+
from typing import Callable
23

34
import pytest
45
from inline_snapshot import snapshot
@@ -255,3 +256,65 @@ def test_canonicalize_no_traceback():
255256
builtins.ValueError
256257
----\
257258
""")
259+
260+
261+
def test_recursion():
262+
def foo(b: Callable[[], None]):
263+
b()
264+
foo(b)
265+
266+
def foo2():
267+
foo(foo2)
268+
269+
def bar():
270+
baz()
271+
272+
def baz():
273+
pass
274+
275+
try:
276+
foo(bar)
277+
except Exception as e:
278+
assert canonicalize_exception_traceback(e).replace(__file__, '__file__') == snapshot("""\
279+
280+
builtins.RecursionError
281+
----
282+
tests.test_canonicalize_exception.test_recursion
283+
foo(bar)
284+
tests.test_canonicalize_exception.foo
285+
foo(b)
286+
287+
<recursion detected>\
288+
""")
289+
290+
try:
291+
foo(baz)
292+
except Exception as e:
293+
assert canonicalize_exception_traceback(e).replace(__file__, '__file__') == snapshot("""\
294+
295+
builtins.RecursionError
296+
----
297+
tests.test_canonicalize_exception.test_recursion
298+
foo(baz)
299+
tests.test_canonicalize_exception.foo
300+
foo(b)
301+
302+
<recursion detected>\
303+
""")
304+
305+
try:
306+
foo2()
307+
except Exception as e:
308+
assert canonicalize_exception_traceback(e).replace(__file__, '__file__') == snapshot("""\
309+
310+
builtins.RecursionError
311+
----
312+
tests.test_canonicalize_exception.test_recursion
313+
foo2()
314+
tests.test_canonicalize_exception.foo2
315+
foo(foo2)
316+
tests.test_canonicalize_exception.foo
317+
b()
318+
319+
<recursion detected>\
320+
""")

0 commit comments

Comments
 (0)