Customizing stackscope for your library¶
stackscope
contains several customization hooks that allow it to be
adapted to provide good stack traces for context managers, awaitables,
and control-flow primitives that it doesn’t natively know anything about.
These are implemented either as @functools.singledispatch
functions (which dispatch to a different implementation depending on the type of
their first argument) or as @stackscope.lowlevel.code_dispatch
functions (which dispatch to a different
implementation depending on the identity of the Python code object associated
with their first argument). Implementations of these hooks that support a
particular library are referred to as stackscope “glue” for that library.
If you’re working with a library that could be better-supported by stackscope, you have two options for implementing that support:
If you maintain the library that the customizations are intended to support, then define a function named
_stackscope_install_glue_
at top level in any of your library’s module(s), which takes no arguments and returns None. The body of the function should register customization hooks appropriate to your library, using the stackscope APIs described in the rest of this section. As long as you only writeimport stackscope
inside the body of the glue installation function, this won’t require that users of your library install stackscope, but they will benefit from your glue if they do.If you’re contributing glue for a library you don’t maintain, you can put the glue in stackscope instead. We have glue for several modules and a system that avoids registering it unless the module has been imported. See
stackscope/_glue.py
, and feel free to submit a PR.
If the same module has glue implemented using both of these methods, then the glue provided by the module will be used; the glue shipped with stackscope is ignored. This allows for a module’s glue to start out being shipped with stackscope and later “graduate” to being maintained upstream.
Overview of customization hooks¶
In order to understand the available customization hooks, it’s helpful to know how stackscope’s stack extraction works internally. There are separate systems for frames and for context managers, and they can interact with each other recursively.
Frames¶
There are two hooks that are relevant in determining the frames in the
returned stack: unwrap_stackitem()
and elaborate_frame()
.
unwrap_stackitem()
receives a “stack item”, which may have been
passed to stackscope.extract()
or returned from an
elaborate_frame()
or unwrap_stackitem()
hook. It is
dispatched based on the Python type of the stack item. Abstractly
speaking, a stack item should be something that logically has Python
frame objects associated with it, and the job of
unwrap_stackitem()
is to turn it into something that is closer
to those frame objects. unwrap_stackitem()
may return a single
stack item, a sequence thereof, or None if it can’t do any unwrapping.
Each returned stack item is recursively unwrapped in the same way until
no further unwrapping can be done. The resulting frame objects become the
basis for the Stack.frames
and any non-frame-objects go in Stack.leaf
.
Example: built-in glue for unwrapping a generator iterator:
@unwrap_stackitem.register(types.GeneratorType)
def unwrap_geniter(gen: types.GeneratorType[Any, Any, Any]) -> Any:
if gen.gi_running:
return StackSlice(outer=gen.gi_frame)
return (gen.gi_frame, gen.gi_yieldfrom)
elaborate_frame()
operates after unwrapping is complete, and is
dispatched based on the code object identity of the executing frame,
so it’s useful for customizations that are specific to a particular
function. (You get approximately one code object per source file
location of a function definition.) It receives the frame it is
elaborating as well as the next inner frame-or-leaf for context. It
can customize the Frame
object, such as by setting the Frame.hide
attribute or modifying the Frame.contexts
. It can also redirect the
rest of the stack extraction, by returning a stack item or sequence of
stack items that will be used in place of the otherwise-next frame and
all of its callees. (If it returns a sequence that ends with the
otherwise-next frame, then the preceding elements are inserted before
the rest of the stack trace rather than replacing it.)
Example: glue for elaborating greenback.await_()
:
@elaborate_frame.register(greenback.await_)
def elaborate_greenback_await(
frame: Frame, next_inner: object
) -> object:
frame.hide = True
if (
isinstance(next_inner, Frame)
and next_inner.pyframe.f_code.co_name != "switch"
):
# await_ that's not suspended at greenlet.switch() requires
# no special handling
return None
# Greenback-mediated await of async function from sync land.
# If we have a coroutine to descend into, do so;
# otherwise the traceback will unhelpfully stop here.
# This works whether the coro is running or not.
# (The only way to get coro=None is if we're taking
# the traceback in the early part of await_() before
# coro is assigned.)
return frame.pyframe.f_locals.get("coro")
Contexts¶
There are three hooks relevant in determining the context
managers in the returned stack: unwrap_context()
,
unwrap_context_generator()
, and
elaborate_context()
. They are less complex than the frame hooks
since they only operate on one context manager at a time.
unwrap_context()
handles context managers that wrap other context
managers. It receives the “outer” context manager and returns the “inner”
one, or returns None to indicate no further unwrapping is needed. It is
dispatched based on the type of the outer context manager. When using
unwrap_context()
, the “outer” context manager is totally lost; it
appears to stackscope, and to clients of extract()
, as though only
the “inner” one ever existed. If you prefer to include both, you can
assign to the Context.children
attribute in elaborate_context()
instead.
Example: glue for elaborating greenback.async_context
objects:
@unwrap_context.register(greenback.async_context)
def unwrap_greenback_async_context(manager: Any) -> Any:
return manager._cm
unwrap_context_generator()
is a specialization of
unwrap_context()
for generator-based context managers
(@contextmanager
and
@asynccontextmanager
). It works
exactly like unwrap_context()
except that it takes as its first
argument the generator’s Stack
rather than the context manager
object. Like elaborate_frame()
, unwrap_context_generator()
is dispatched based on the code object identity of the function that
implements the context manager, so you can unwrap different generator-based
context managers in different ways even though their context manager
objects all have the same type.
elaborate_context()
is called once for each context manager
before trying to unwrap it, and again after each successful
unwrapping. It is dispatched based on the context manager type and
fills in attributes of the Context
object, such as
Context.description
, Context.children
, and
Context.inner_stack
. These Context
attributes might be filled out
using calls to fill_context()
or extract()
, which will
recursively execute context/frame hooks as
needed. elaborate_context()
can also change the Context.obj
which may influence further unwrapping attempts.
Example: glue for handling generator-based @contextmanager
s:
@elaborate_context.register(contextlib._GeneratorContextManagerBase)
def elaborate_generatorbased_contextmanager(mgr: Any, context: Context) -> None:
# Don't descend into @contextmanager frames if the context manager
# is currently exiting, since we'll see them later in the traceback
# anyway
if not context.is_exiting:
context.inner_stack = stackscope.extract(mgr.gen)
context.description = f"{mgr.gen.__qualname__}(...)"
Utilities for use in customization hooks¶
- stackscope.extract_child(stackitem: StackItem, *, for_task: bool) Stack ¶
Perform a recursive call equivalent to
extract(stackitem)
, but reusing the options that were passed to the originalextract()
. You should only call this from within a customization hook such aselaborate_context()
.If for_task is True, then this nested stackitem is considered to represent an async child task. Its stack will be fully extracted only if the outer
extract()
call specifiedrecurse_child_tasks=True
; otherwise you will get a stubStack
with aroot
but noframes
.
- stackscope.fill_context(context: Context) None ¶
Augment the given newly-constructed
Context
object using the context manager hooks (unwrap_context()
andelaborate_context()
), calling both hooks in a loop until a steady state is reached.
Customization hooks reference¶
- stackscope.unwrap_stackitem(item: StackItem) StackItem | Sequence[StackItem] | FrameIterator[StackItem] | None ¶
Hook for turning something encountered during stack traversal into one or more objects that are easier to understand than it, eventually resulting in a Python frame. May return a single object, a sequence of objects, or None if we don’t know how to unwrap any further. When extracting a stack,
unwrap_stackitem()
will be applied repeatedly until a series of frames results.The built-in unwrapping rules serve as good examples here:
Unwrapping a coroutine object, generator iterator, or async generator iterator produces a tuple
(frame, next)
where frame is a Python frame object and next is the thing being awaited or yielded-from (potentially another coroutine object, generator iterator, etc).Unwrapping a
threading.Thread
produces the sequence of frames that form the thread’s stack, from outermost to innermost.Unwrapping an async generator
asend()
orathrow()
awaitable produces the async generator it is operating on.Unwrapping a
coroutine_wrapper
object produces the coroutine it is wrapping. This allows stackscope to look inside most awaitable objects. (A “coroutine wrapper” is the object returned when you callcoro.__await__()
on a coroutine object, which acts like a coroutine object except that it also implements__next__
.)
- stackscope.yields_frames(fn: Callable[[P], Iterator[T]]) Callable[[P], FrameIterator[T]] ¶
Decorator for an
unwrap_stackitem()
implementation which allows the series of stack items to be yielded one-by-one instead of returned in a sequence.The main benefit of this approach is that previously-yielded frames can be preserved even if an exception is raised. It is used by the built-in glue that handles
StackSlice
objects. A decorator is needed to distinguish an iterator of stack items (which should be unpacked and treated one-by-one) from a generator iterator with a different purpose (which is a common stack item that we definitely should not iterate over).Put
@yields_frames
underneath@unwrap_stackitem.register
.
- stackscope.elaborate_frame(frame: Frame, next_inner: object) StackItem | Sequence[StackItem] | None ¶
Hook for providing additional information about a frame encountered during stack traversal. This hook uses
@code_dispatch
, so it can be customized based on which function the frame is executing.next_inner is the thing that the frame is currently busy with: either the next
Frame
in the list ofStack.frames
, or else theStack.leaf
(which may beNone
) if there is no next frame.The
elaborate_frame()
hook may modify the attributes of frame, such as by settingFrame.hide
. It may also redirect the remainder of the stack trace, by returning an object or sequence of objects that should be unwrapped to become the new next_inner. If the return value is a sequence and it ends with next_inner, then the items before next_inner are inserted before the remainder of the stack trace instead of replacing it. If you don’t want to affect the rest of the stack trace, then return None (equivalent to next_inner). If you want to remove the rest of the stack trace and not replace it with anything, then returnPRUNE
(which is equivalent to an empty tuple).
- stackscope.PRUNE¶
Sentinel value which may be returned by
elaborate_frame()
to indicate that the remainder of the stack (containing the direct and indirect callees of the frame being elaborated) should not be included in the extractedstackscope.Stack
.
- stackscope.customize(target: Any = None, *inner_names: str, hide: bool = False, hide_line: bool = False, prune: bool = False, elaborate: Callable[[Frame, object], object] | None = None) Any ¶
Shorthand for common
elaborate_frame()
customizations which affect how stack extraction interacts with invocations of specific functions.(target, *inner_names)
identifies a code object; all frames executing that code object will receive the customizations specified by the keyword arguments tocustomize()
. Typically you would a function as target, with no inner_names, to customize frames that are executing that function. You only need inner_names if you’re trying to name a nested function; seeget_code()
for details.If you don’t specify a target, then
customize()
returns a partially bound invocation of itself so that you can use it as a decorator. The target in that case is the decorated function; the decorator returns that function unchanged. (Note the distinction:@customize
decorates the function whose frames get the custom behavior, while@elaborate_frame.register
decorates the function that implements the custom behavior.)The customizations are specified by the keyword arguments you pass:
If elaborate is specified, then it will be registered as an
elaborate_frame
hook for the matching frames.If hide is True, then the matching frames will have their
Frame.hide
attribute set to True, indicating that they should not be shown by default when printing the stack.If hide_line is True, then the matching frames will have their
Frame.hide_line
attribute set to True, indicating that the executing line should not be shown by default when printing the stack (but the function info and contexts still will be).If prune is True, then direct and indirect callees of the matching frames will not be included when extracting the stack. This option only has effect if elaborate either is unspecified or returns None.
- stackscope.unwrap_context(manager: ContextManager[Any] | AsyncContextManager[Any], context: Context) None | ContextManager[Any] | AsyncContextManager[Any] | tuple[()] ¶
Hook for extracting an inner context manager from another context manager that wraps it. The stackscope context object is also provided in case it’s useful. Return None if there is no further unwrapping to do. Return
PRUNE
(equivalent to an empty tuple) to hide this context manager from the traceback. Unlikeunwrap_stackitem()
, it is not currently supported to let the result of unwrapping a context manager be a sequence of multiple context managers.Note
If the original context manager is currently exiting, the frames implementing its
__exit__
will appear on the stack regardless of any unwrapping you do here. You can customizeelaborate_frame()
for the appropriate__exit__
if you want to affect the display there as well.
- stackscope.unwrap_context_generator(frame: Frame, context: Context) None | ContextManager[Any] | AsyncContextManager[Any] | tuple[()] ¶
Hook for extracting an inner context manager from the outermost frame of a generator-based context manager that wraps it. This hook uses
@code_dispatch
, so it can be customized based on the identity of the function that implements the context manager. Apart from that, its semantics are equivalent tounwrap_context()
.Note
If the context manager you’re unwrapping uses
yield from
, it’s possible that you’ll need to access callees of frame to implement your logic. You can find these usingContext.inner_stack
, or if that’s None because the context is currently exiting, you can reconstruct it usingstackscope.extract(context.obj.gen)
.
- stackscope.elaborate_context(manager: ContextManager[Any] | AsyncContextManager[Any], context: Context) None ¶
Hook for providing additional information about a context manager encountered during stack traversal. It should modify the attributes of the provided context object (a
Context
) based on the provided manager (the actual context manager, i.e., the thing whose type has__enter__
and__exit__
attributes).