@@ -42,7 +42,7 @@ def stacks_length(self):
4242
4343 def __repr__ (self ):
4444 if len (self .stacks ) == 0 :
45- return self .msg
45+ return self .msg or ""
4646
4747 def _repr (v ):
4848 try :
@@ -60,7 +60,9 @@ def _repr(v):
6060 for k , v in local_variables .items ():
6161 content .append (' --> %s = %s' % (k , _repr (v )))
6262 content .append ('' )
63- content .append ("%s: %s" % (self .exc_type .__name__ , self .msg ))
63+
64+ exc_name = self .exc_type .__name__ if self .exc_type else "Exception"
65+ content .append ("%s: %s" % (exc_name , self .msg ))
6466
6567 return "\n " .join (content )
6668
@@ -135,3 +137,126 @@ class RQApiNotSupportedError(RQUserError):
135137class RQDatacVersionTooLow (RuntimeError ):
136138 pass
137139
140+
141+ # ExceptionGroup implementation for compatibility with older Python versions
142+ # Based on Python 3.11's ExceptionGroup behavior
143+ class BaseExceptionGroup (BaseException ):
144+ """A base class for grouping multiple exceptions."""
145+
146+ def __init__ (self , message , exceptions ):
147+ if not isinstance (message , str ):
148+ raise TypeError (f"ExceptionGroup message must be a string, not { type (message ).__name__ } " )
149+
150+ if not exceptions :
151+ raise ValueError ("second argument (exceptions) must be a non-empty sequence" )
152+
153+ # Convert to list and validate exceptions
154+ exceptions_list = []
155+ for exc in exceptions :
156+ if isinstance (exc , BaseException ):
157+ exceptions_list .append (exc )
158+ elif isinstance (exc , type ) and issubclass (exc , BaseException ):
159+ # Allow exception classes, instantiate them
160+ exceptions_list .append (exc ())
161+ else :
162+ raise ValueError (f"Item { exc !r} of second argument is not an exception" )
163+
164+ self .message = message
165+ self .exceptions = tuple (exceptions_list )
166+ super ().__init__ (message )
167+
168+ def __str__ (self ):
169+ if len (self .exceptions ) == 1 :
170+ return f"{ self .message } (1 sub-exception)"
171+ return f"{ self .message } ({ len (self .exceptions )} sub-exceptions)"
172+
173+ def __repr__ (self ):
174+ return f"{ self .__class__ .__name__ } ({ self .message !r} , { list (self .exceptions )!r} )"
175+
176+ def split (self , condition ):
177+ """Split the exception group based on a condition.
178+
179+ Args:
180+ condition: A callable that takes an exception and returns True/False,
181+ or an exception type/tuple of types.
182+
183+ Returns:
184+ A tuple of (matching_group, non_matching_group).
185+ Either element can be None if no exceptions match that category.
186+ """
187+ if isinstance (condition , type ) or (isinstance (condition , tuple ) and
188+ all (isinstance (t , type ) for t in condition )):
189+ # Handle exception type(s)
190+ def check_condition (exc ):
191+ return isinstance (exc , condition )
192+ elif callable (condition ):
193+ def check_condition (exc ):
194+ result = condition (exc )
195+ return bool (result )
196+ else :
197+ raise TypeError ("condition must be a callable or exception type(s)" )
198+
199+ matching = []
200+ non_matching = []
201+
202+ for exc in self .exceptions :
203+ if isinstance (exc , BaseExceptionGroup ):
204+ # Recursively split nested groups
205+ match_group , non_match_group = exc .split (condition )
206+ if match_group is not None :
207+ matching .append (match_group )
208+ if non_match_group is not None :
209+ non_matching .append (non_match_group )
210+ else :
211+ if check_condition (exc ):
212+ matching .append (exc )
213+ else :
214+ non_matching .append (exc )
215+
216+ matching_group = None
217+ if matching :
218+ matching_group = self .derive (matching )
219+
220+ non_matching_group = None
221+ if non_matching :
222+ non_matching_group = self .derive (non_matching )
223+
224+ return (matching_group , non_matching_group )
225+
226+ def subgroup (self , condition ):
227+ """Return a subgroup containing only exceptions that match the condition."""
228+ matching_group , _ = self .split (condition )
229+ return matching_group
230+
231+ def derive (self , exceptions ):
232+ """Create a new exception group with the same message but different exceptions."""
233+ if not exceptions :
234+ return None
235+ return self .__class__ (self .message , exceptions )
236+
237+
238+ class ExceptionGroup (BaseExceptionGroup , Exception ):
239+ """An exception group that inherits from Exception."""
240+ pass
241+
242+
243+ def format_exception_group (exc_group , indent = "" ):
244+ """Format an ExceptionGroup for display."""
245+ if not isinstance (exc_group , BaseExceptionGroup ):
246+ return str (exc_group )
247+
248+ lines = [f"{ indent } { exc_group .__class__ .__name__ } : { exc_group .message } " ]
249+
250+ for i , exc in enumerate (exc_group .exceptions ):
251+ is_last = (i == len (exc_group .exceptions ) - 1 )
252+ prefix = "└─ " if is_last else "├─ "
253+ child_indent = " " if is_last else "│ "
254+
255+ if isinstance (exc , BaseExceptionGroup ):
256+ lines .append (f"{ indent } { prefix } { format_exception_group (exc , indent + child_indent )} " )
257+ else :
258+ exc_str = f"{ exc .__class__ .__name__ } : { exc } "
259+ lines .append (f"{ indent } { prefix } { exc_str } " )
260+
261+ return "\n " .join (lines )
262+
0 commit comments