Repository navigation
Conversation
wrap_error() builds a subclass of the original exception and instantiates it with a single message argument. BaseExceptionGroup.__new__ takes the sub-exceptions as well, so every epy.reraise()/maybe_reraise() over an ExceptionGroup replaced the original error with TypeError: BaseExceptionGroup.__new__() takes exactly 2 arguments (1 given) losing the failure it was adding context to. Groups reach this path through ordinary code: asyncio.TaskGroup raises one for any task failure. Rebuild groups with their own constructor, keep their __str__ (so the sub-exception count is not duplicated), and wrap the message from .message rather than str(e). Also init through BaseException, so exceptions outside the Exception branch can be wrapped.
ADITYA-KUMAR-2358
left a comment
There was a problem hiding this comment.
Reviewed the changes. The implementation correctly adds support for ExceptionGroup and BaseException handling, and the added tests cover message formatting, exception preservation, subclass handling, and KeyboardInterrupt. Everything looks good from my side.
There was a problem hiding this comment.
Looks good. The change correctly handles ExceptionGroup construction and formatting while preserving the existing behavior for regular and BaseException types. Using e.message also avoids duplicating the sub-exception count in the wrapped message.
There was a problem hiding this comment.
The added tests provide good coverage for the new exception handling behavior. They verify correct formatting and preservation of sub-exceptions for ExceptionGroup, preservation of custom ExceptionGroup subclasses, and support for BaseException types such as KeyboardInterrupt. The assertions around the group message and unchanged sub-exceptions are particularly useful for guarding against regressions.
Problem
epy.reraise()/epy.maybe_reraise()destroy the exception they are supposed to annotate when it is anExceptionGroup:wrap_error()createsclass WrappedException(type(e))and callsWrappedException(msg). That inherited__new__isBaseExceptionGroup.__new__, which requires the sub-exceptions too, so construction raises and the caller sees aTypeErrorfrom inside etils instead of the real failure — with the sub-exceptions gone.This is reachable from ordinary code:
asyncio.TaskGroupraises anExceptionGroupfor any failing task, andexcept*handlers re-raise them, so anepy.maybe_reraiseblock around async work turns a real error into thisTypeError.Change
type(e).__new__(cls, msg, e.exceptions)and the matching__init__), so the sub-exceptions survive.__str__/__repr__instead ofBaseException's, and build the message frome.messagerather thanstr(e), so the(N sub-exceptions)suffix appears once rather than twice.BaseException.__init__rather thanException.__init__, so exceptions outside theExceptionbranch (KeyboardInterrupt,…) can be wrapped too. Previously that raisedTypeError: descriptor '__init__' requires a 'Exception' object.__new__is only overridden for groups —OSErrorand friends need their own inherited__new__, which is why it cannot be replaced unconditionally.Tests
Three tests added: a plain
ExceptionGroup, a user subclass of it, and aBaseException. All three fail onmain:and pass with the change (6 passed). The rest of
etils/epyis unchanged by it; the 8 failures I see there locally are missingtensorflow_datasetsin my environment, inlazy_imports_utils_test.py.