Skip to content
This repository was archived by the owner on Jan 23, 2026. It is now read-only.

Commit 54124f2

Browse files
NickCaogithub-actions[bot]
authored andcommitted
Add leaf_exceptions helper
(cherry picked from commit b811f04)
1 parent 6f91450 commit 54124f2

1 file changed

Lines changed: 61 additions & 0 deletions

File tree

  • packages/jumpstarter-cli-common/jumpstarter_cli_common

packages/jumpstarter-cli-common/jumpstarter_cli_common/exceptions.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import types
12
from functools import wraps
3+
from types import TracebackType
24

35
import asyncclick as click
46

@@ -42,3 +44,62 @@ def wrapped(*args, **kwargs):
4244
raise
4345

4446
return wrapped
47+
48+
49+
# https://peps.python.org/pep-0785/#reference-implementation
50+
def leaf_exceptions(self: BaseExceptionGroup, *, fix_tracebacks: bool = True) -> list[BaseException]:
51+
"""
52+
Return a flat list of all 'leaf' exceptions.
53+
54+
If fix_tracebacks is True, each leaf will have the traceback replaced
55+
with a composite so that frames attached to intermediate groups are
56+
still visible when debugging. Pass fix_tracebacks=False to disable
57+
this modification, e.g. if you expect to raise the group unchanged.
58+
"""
59+
60+
def _flatten(group: BaseExceptionGroup, parent_tb: TracebackType | None = None):
61+
group_tb = group.__traceback__
62+
combined_tb = _combine_tracebacks(parent_tb, group_tb)
63+
result = []
64+
for exc in group.exceptions:
65+
if isinstance(exc, BaseExceptionGroup):
66+
result.extend(_flatten(exc, combined_tb))
67+
elif fix_tracebacks:
68+
tb = _combine_tracebacks(combined_tb, exc.__traceback__)
69+
result.append(exc.with_traceback(tb))
70+
else:
71+
result.append(exc)
72+
return result
73+
74+
return _flatten(self)
75+
76+
77+
def _combine_tracebacks(
78+
tb1: TracebackType | None,
79+
tb2: TracebackType | None,
80+
) -> TracebackType | None:
81+
"""
82+
Combine two tracebacks, putting tb1 frames before tb2 frames.
83+
84+
If either is None, return the other.
85+
"""
86+
if tb1 is None:
87+
return tb2
88+
if tb2 is None:
89+
return tb1
90+
91+
# Convert tb1 to a list of frames
92+
frames = []
93+
current = tb1
94+
while current is not None:
95+
frames.append((current.tb_frame, current.tb_lasti, current.tb_lineno))
96+
current = current.tb_next
97+
98+
# Create a new traceback starting with tb2
99+
new_tb = tb2
100+
101+
# Add frames from tb1 to the beginning (in reverse order)
102+
for frame, lasti, lineno in reversed(frames):
103+
new_tb = types.TracebackType(tb_next=new_tb, tb_frame=frame, tb_lasti=lasti, tb_lineno=lineno)
104+
105+
return new_tb

0 commit comments

Comments
 (0)