Skip to content

Commit f764000

Browse files
committed
fix equip_with_traceback (hopefully)
1 parent f40ae4c commit f764000

2 files changed

Lines changed: 39 additions & 15 deletions

File tree

unpythonic/conditions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ def canonize(exc):
175175
# This only works on Python 3.7 and later, because we need to create a traceback object in pure Python code.
176176
try:
177177
# In the result, omit equip_with_traceback() and signal().
178-
condition = equip_with_traceback(condition, depth=2)
178+
condition = equip_with_traceback(condition, stacklevel=2)
179179
except NotImplementedError: # pragma: no cover
180180
pass # well, we tried!
181181

unpythonic/misc.py

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -349,10 +349,21 @@ def isexceptiontype(exc):
349349
if finallyf is not None:
350350
finallyf()
351351

352-
def equip_with_traceback(exc, depth=0): # Python 3.7+
352+
def equip_with_traceback(exc, stacklevel=1): # Python 3.7+
353353
"""Given an exception instance exc, equip it with a traceback.
354354
355-
`depth` is the starting depth for `sys._getframe`.
355+
`stacklevel` is the starting depth below the top of the call stack,
356+
to cull useless detail:
357+
- `0` means the trace includes everything, also
358+
`equip_with_traceback` itself,
359+
- `1` means the trace includes everything up to the caller,
360+
- And so on.
361+
362+
So typically, for direct use of this function `stacklevel` should
363+
be `1` (so it excludes `equip_with_traceback` itself, but shows
364+
all stack levels from your code), and for use in a utility function
365+
that itself is called from your code, it should be `2` (so it excludes
366+
the utility function, too).
356367
357368
The return value is `exc`, with its traceback set to the produced
358369
traceback.
@@ -361,13 +372,12 @@ def equip_with_traceback(exc, depth=0): # Python 3.7+
361372
362373
When not supported, raises `NotImplementedError`.
363374
364-
This is useful in some special cases only, mainly when `raise` cannot be
365-
used for some reason, and a manually created exception instance needs a
366-
traceback. (E.g. in implementing the system for conditions and restarts.)
375+
This is useful mainly in special cases, where `raise` cannot be used for
376+
some reason, and a manually created exception instance needs a traceback.
377+
(The `signal` function in the conditions-and-restarts system uses this.)
367378
368-
The `sys._getframe` function exists in CPython and in PyPy3,
369-
but for another arbitrary Python implementation this is not
370-
guaranteed.
379+
**CAUTION**: The `sys._getframe` function exists in CPython and in PyPy3,
380+
but for another arbitrary Python implementation this is not guaranteed.
371381
372382
Based on solution by StackOverflow user Zbyl:
373383
https://stackoverflow.com/a/54653137
@@ -379,23 +389,37 @@ def equip_with_traceback(exc, depth=0): # Python 3.7+
379389
"""
380390
if not isinstance(exc, BaseException):
381391
raise TypeError("exc must be an exception instance; got {} with value '{}'".format(type(exc), exc))
392+
if not isinstance(stacklevel, int):
393+
raise TypeError("stacklevel must be int, got {} with value '{}'".format(type(stacklevel), stacklevel))
394+
if stacklevel < 0:
395+
raise ValueError("stacklevel must be >= 0, got {}".format(stacklevel))
382396

383397
try:
384398
getframe = sys._getframe
385399
except AttributeError as err: # pragma: no cover, both CPython and PyPy3 have sys._getframe.
386400
raise NotImplementedError("Need a Python interpreter which has `sys._getframe`") from err
387401

388-
tb = None
402+
frames = []
403+
depth = stacklevel
389404
while True:
390-
# Starting from given depth, get all frames up to the root of the call stack.
391405
try:
392-
frame = getframe(depth)
406+
frames.append(getframe(depth)) # 0 = top of call stack
393407
depth += 1
394-
except ValueError:
408+
except ValueError: # beyond the root level
395409
break
396-
# Python 3.7+ allows creating types.TracebackType objects from Python code.
410+
411+
# Python 3.7+ allows creating `types.TracebackType` objects in Python code.
397412
try:
398-
tb = TracebackType(tb, frame, frame.f_lasti, frame.f_lineno)
413+
tracebacks = []
414+
nxt = None # tb_next should point toward the level where the exception occurred.
415+
for frame in frames: # walk from top of call stack toward the root
416+
tb = TracebackType(nxt, frame, frame.f_lasti, frame.f_lineno)
417+
tracebacks.append(tb)
418+
nxt = tb
419+
if tracebacks:
420+
tb = tracebacks[-1] # root level
421+
else:
422+
tb = None
399423
except TypeError as err: # Python 3.6 or earlier
400424
raise NotImplementedError("Need Python 3.7 or later to create traceback objects") from err
401425
return exc.with_traceback(tb) # Python 3.7+

0 commit comments

Comments
 (0)