Skip to content

Commit 178f9a0

Browse files
joelostblompre-commit-ci[bot]lwjohnst86
authored
feat: ✨ generalize beautification of errors (#309)
# Description It is helpful to remove the traceback for more errors than just the datapackage errors. Needs an in-depth review. ## Checklist - [x] Formatted Markdown - [x] Ran `just run-all` --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Luke W. Johnston <lwjohnst86@users.noreply.github.com> Co-authored-by: Luke W. Johnston <lwjohnst@gmail.com>
1 parent c731dfc commit 178f9a0

File tree

4 files changed

+353
-34
lines changed

4 files changed

+353
-34
lines changed

src/check_datapackage/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@
22

33
from rich import print as pretty_print
44

5-
from .check import DataPackageError, check, explain
5+
from .check import (
6+
DataPackageError,
7+
check,
8+
explain,
9+
)
610
from .config import Config
711
from .examples import (
812
example_field_properties,

src/check_datapackage/check.py

Lines changed: 112 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -28,33 +28,89 @@
2828
from check_datapackage.issue import Issue
2929
from check_datapackage.read_json import read_json
3030

31+
# Type alias for Python exception hook
32+
PythonExceptionHook = Callable[
33+
[type[BaseException], BaseException, Optional[TracebackType]],
34+
None,
35+
]
36+
37+
# Type alias for IPython custom exception handler (includes self and tb_offset)
38+
IPythonExceptionHandler = Callable[
39+
[Any, type[BaseException], BaseException, Optional[TracebackType], None],
40+
Optional[list[str]],
41+
]
42+
3143

3244
def _pretty_print_exception(
3345
exc_type: type[BaseException],
3446
exc_value: BaseException,
3547
) -> None:
36-
# Print the error type and message, without traceback
37-
return rprint(f"\n[red]{exc_type.__name__}[/red]: {exc_value}")
48+
rprint(f"\n[red]{exc_type.__name__}[/red]: {exc_value}")
3849

3950

40-
def no_traceback_hook(
41-
exc_type: type[BaseException],
42-
exc_value: BaseException,
43-
exc_traceback: TracebackType | None,
44-
) -> None:
45-
"""Exception hook to hide tracebacks for DataPackageError."""
46-
if issubclass(exc_type, DataPackageError):
47-
_pretty_print_exception(exc_type, exc_value)
48-
else:
49-
sys.__excepthook__(exc_type, exc_value, exc_traceback)
51+
def _create_suppressed_traceback_hook(
52+
exception_types: tuple[type[BaseException], ...],
53+
old_hook: PythonExceptionHook,
54+
) -> PythonExceptionHook:
55+
"""Create a Python exception hook that suppresses tracebacks.
5056
57+
Args:
58+
exception_types: Exception types to suppress tracebacks for.
59+
old_hook: The previous exception hook to delegate unregistered exceptions to.
5160
52-
# Need to use a custom exception hook to hide tracebacks for our custom exceptions
53-
sys.excepthook = no_traceback_hook
61+
Returns:
62+
A composable exception hook function.
63+
"""
64+
65+
def hook(
66+
exc_type: type[BaseException],
67+
exc_value: BaseException,
68+
exc_traceback: Optional[TracebackType],
69+
) -> None:
70+
if issubclass(exc_type, exception_types):
71+
_pretty_print_exception(exc_type, exc_value)
72+
else:
73+
old_hook(exc_type, exc_value, exc_traceback)
74+
75+
return hook
76+
77+
78+
def _create_suppressed_traceback_ipython_hook(
79+
exception_types: tuple[type[BaseException], ...],
80+
old_custom_tb: Optional[IPythonExceptionHandler],
81+
) -> Callable[
82+
[Any, type[BaseException], BaseException, Optional[TracebackType], None],
83+
Optional[list[str]],
84+
]:
85+
"""Create an IPython exception hook that suppresses tracebacks.
86+
87+
Args:
88+
exception_types: Exception types to suppress tracebacks for.
89+
old_custom_tb: The previous IPython custom exception handler, if any.
90+
91+
Returns:
92+
A composable IPython exception hook function.
93+
"""
94+
has_old_handler = old_custom_tb is not None
95+
96+
def hook(
97+
self: Any,
98+
exc_type: type[BaseException],
99+
exc_value: BaseException,
100+
exc_traceback: Optional[TracebackType],
101+
tb_offset: None = None,
102+
) -> Optional[list[str]]:
103+
if issubclass(exc_type, exception_types):
104+
_pretty_print_exception(exc_type, exc_value)
105+
return []
106+
elif has_old_handler and old_custom_tb is not None:
107+
return old_custom_tb(self, exc_type, exc_value, exc_traceback, tb_offset)
108+
else:
109+
return None
110+
111+
return hook
54112

55113

56-
# Unfortunately, IPython uses its own exception handling mechanism,
57-
# so we need to set a separate custom exception handler there.
58114
def _is_running_from_ipython() -> bool:
59115
"""Checks whether running in IPython interactive console or not."""
60116
try:
@@ -65,25 +121,44 @@ def _is_running_from_ipython() -> bool:
65121
return get_ipython() is not None # type: ignore[no-untyped-call]
66122

67123

68-
if _is_running_from_ipython():
124+
def _setup_suppressed_tracebacks(
125+
*exception_types: type[BaseException],
126+
) -> None:
127+
"""Set up exception hooks to hide tracebacks for specified exceptions.
69128
70-
def no_traceback_in_ipython(
71-
self: Any,
72-
exc_type: type[BaseException],
73-
exc_value: BaseException,
74-
exc_traceback: TracebackType | None,
75-
tb_offset: None = None,
76-
) -> None:
77-
"""Hide tracebacks and correctly display rich markup in IPython."""
78-
if issubclass(exc_type, DataPackageError):
79-
_pretty_print_exception(exc_type, exc_value)
80-
else:
81-
# Regular IPython traceback
82-
self.showtraceback(
83-
(exc_type, exc_value, exc_traceback), tb_offset=tb_offset
84-
)
129+
This function is composable - multiple calls add to the existing hook
130+
rather than replacing it. Each package only needs to register its own
131+
exceptions.
85132
86-
get_ipython().set_custom_exc((Exception,), no_traceback_in_ipython) # type: ignore # noqa: F821
133+
Args:
134+
*exception_types: Exception types to hide tracebacks for.
135+
136+
Raises:
137+
TypeError: If any exception_type is not an exception class.
138+
139+
Examples:
140+
```python
141+
# In package A
142+
_setup_suppressed_tracebacks(ErrorA)
143+
144+
# In package B - adds to existing hook
145+
_setup_suppressed_tracebacks(ErrorB, ErrorC)
146+
# Now ErrorA, ErrorB, and ErrorC will all have suppressed tracebacks
147+
```
148+
"""
149+
for exc_type in exception_types:
150+
if not (isinstance(exc_type, type) and issubclass(exc_type, BaseException)):
151+
raise TypeError(f"{exc_type!r} is not an exception class")
152+
153+
sys.excepthook = _create_suppressed_traceback_hook(exception_types, sys.excepthook)
154+
155+
if _is_running_from_ipython():
156+
ip = get_ipython() # type: ignore # noqa: F821
157+
old_custom_tb: Optional[IPythonExceptionHandler] = getattr(ip, "CustomTB", None)
158+
ip.set_custom_exc(
159+
(Exception,),
160+
_create_suppressed_traceback_ipython_hook(exception_types, old_custom_tb),
161+
)
87162

88163

89164
class DataPackageError(Exception):
@@ -930,3 +1005,7 @@ def _get_errors_in_group(
9301005

9311006
def _strip_index(jsonpath: str) -> str:
9321007
return re.sub(r"\[\d+\]$", "", jsonpath)
1008+
1009+
1010+
# Set up exception hooks at module load time
1011+
_setup_suppressed_tracebacks(DataPackageError)

0 commit comments

Comments
 (0)