Coverage for .venv/lib/python3.13/site-packages/litellm/proxy/_experimental/mcp_server/faults/traversal.py: 100%
19 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 12:01 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 12:01 +0000
1"""Shared exception-tree traversal for fault classification.
3Failures cross the MCP SDK's anyio task groups wrapped in ``ExceptionGroup``s and chained through
4``raise ... from`` causes, so every classifier that needs an exception buried in the tree (an
5upstream ``httpx.Response``, a context-window overflow) has to walk the same shapes. One traversal
6with one deliberate order keeps blame assignment consistent across classifiers: explicit links are
7searched before incidental ones, so an exception raised while handling the real failure can never
8shadow the failure itself.
9"""
11from __future__ import annotations
13from collections.abc import Iterator
14from typing import Final
17def iter_exception_tree(exc: BaseException) -> Iterator[BaseException]:
18 """Yield ``exc`` and every exception reachable from it, explicit links first: each node's
19 ``raise ... from`` cause subtree, then ``ExceptionGroup`` members in raise order, then the
20 incidental ``__context__`` chain last. Cycle-safe via identity tracking, and iterative so a
21 deep chain cannot overflow the interpreter stack."""
22 seen: Final[set[int]] = set()
23 stack: Final = [exc]
24 while stack:
25 current = stack.pop()
26 if id(current) in seen:
27 continue
28 seen.add(id(current))
29 yield current
30 if current.__context__ is not None:
31 stack.append(current.__context__)
32 exceptions = getattr(current, "exceptions", None)
33 if isinstance(exceptions, tuple):
34 stack.extend(reversed(exceptions))
35 if current.__cause__ is not None:
36 stack.append(current.__cause__)