2828from check_datapackage .issue import Issue
2929from 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
3244def _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.
58114def _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
89164class DataPackageError (Exception ):
@@ -930,3 +1005,7 @@ def _get_errors_in_group(
9301005
9311006def _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