Coverage for dataexcept/wrapping.py: 100%
28 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-09-03 20:46 +0000
« prev ^ index » next coverage.py v7.15.4, created at 2026-09-03 20:46 +0000
1"""Turning a third-party exception into a DataExcept one.
3The pattern this replaces is everywhere in pipeline code::
5 try:
6 frame = pd.read_csv(path)
7 except OSError as exc:
8 raise DataLoadingError(path, exc) from exc
10It is easy to write and easy to get subtly wrong: forget the ``from exc`` and
11the traceback stops showing what actually failed; pass the original to the
12wrong parameter and it is not recorded at all; catch too broadly and a
13``KeyboardInterrupt`` becomes a data-loading error.
15:func:`wrap` and :func:`wrapping` do the same thing with the wiring settled.
16The original is passed to whichever constructor parameter takes a cause --
17``original``, ``original_exception`` or ``cause``, whichever that class uses --
18and set as ``__cause__`` either way, so a traceback always shows both.
19"""
21from __future__ import annotations
23import contextlib
24import inspect
25from typing import Any, Iterator, Tuple, Type, Union
27from .base import DataExceptError
29__all__ = ["wrap", "wrapping"]
31#: Constructor parameter names used across the package for a wrapped
32#: exception. Checked in this order; the first the target accepts wins.
33_CAUSE_PARAMETERS = ("original", "original_exception", "cause")
35Catchable = Union[Type[BaseException], Tuple[Type[BaseException], ...]]
38def _cause_parameter(target: Type[DataExceptError]) -> str | None:
39 """Return the parameter of *target* that takes a wrapped exception."""
40 try:
41 parameters = inspect.signature(target.__init__).parameters
42 except (TypeError, ValueError): # pragma: no cover - builtins and C types
43 return None
44 for name in _CAUSE_PARAMETERS:
45 if name in parameters:
46 return name
47 return None
50def wrap(
51 original: BaseException,
52 target: Type[DataExceptError],
53 /,
54 **kwargs: Any,
55) -> DataExceptError:
56 """Build *target* from *original*, recording it as the cause.
58 Extra keyword arguments go to the constructor::
60 raise wrap(exc, DataLoadingError, source=path) from exc
62 If *target* accepts a cause parameter, *original* is passed to it. Either
63 way ``__cause__`` is set, so a traceback shows the underlying failure even
64 for a class that records nothing.
66 An explicit ``original``/``cause`` keyword wins, so a caller can still say
67 exactly what they mean.
68 """
69 parameter = _cause_parameter(target)
70 if parameter is not None and parameter not in kwargs:
71 kwargs[parameter] = original
73 exception = target(**kwargs)
74 # Set unconditionally: the target may record nothing, and the point is that
75 # the traceback shows what actually failed.
76 exception.__cause__ = original
77 return exception
80@contextlib.contextmanager
81def wrapping(
82 catch: Catchable,
83 target: Type[DataExceptError],
84 /,
85 **kwargs: Any,
86) -> Iterator[None]:
87 """Translate *catch* raised inside the block into *target*.
89 ::
91 with wrapping(OSError, DataLoadingError, source=path):
92 frame = pd.read_csv(path)
94 Only exceptions matching *catch* are translated; everything else propagates
95 untouched, including anything already raised by this package. Because
96 *catch* is given explicitly there is no default broad ``except``, so a
97 ``KeyboardInterrupt`` or a bug in the block is never relabelled as a data
98 error.
99 """
100 try:
101 yield
102 except catch as exc:
103 raise wrap(exc, target, **kwargs) from exc