From 5655a3e6280351cc612bc7fae6959ae89a3520bb Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Fri, 23 Oct 2020 15:46:33 +0100 Subject: [PATCH 01/73] first attempt at ExceptionGroup/TracebackGroup --- Objects/exceptions.c | 3 +- Python/traceback.c | 8 ++-- exception_group.py | 106 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 113 insertions(+), 4 deletions(-) create mode 100644 exception_group.py diff --git a/Objects/exceptions.c b/Objects/exceptions.c index d4824938a0f507..46c5e1371dfc69 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -227,12 +227,13 @@ BaseException_set_tb(PyBaseExceptionObject *self, PyObject *tb, void *Py_UNUSED( PyErr_SetString(PyExc_TypeError, "__traceback__ may not be deleted"); return -1; } + /* else if (!(tb == Py_None || PyTraceBack_Check(tb))) { PyErr_SetString(PyExc_TypeError, "__traceback__ must be a traceback or None"); return -1; } - + */ Py_INCREF(tb); Py_XSETREF(self->traceback, tb); return 0; diff --git a/Python/traceback.c b/Python/traceback.c index 708678facf7c31..84a7720f17a341 100644 --- a/Python/traceback.c +++ b/Python/traceback.c @@ -115,14 +115,16 @@ tb_next_set(PyTracebackObject *self, PyObject *new_next, void *Py_UNUSED(_)) tb_next_get) */ if (new_next == Py_None) { new_next = NULL; - } else if (!PyTraceBack_Check(new_next)) { + } /* + else if (!PyTraceBack_Check(new_next)) { PyErr_Format(PyExc_TypeError, "expected traceback object, got '%s'", Py_TYPE(new_next)->tp_name); return -1; - } + }*/ /* Check for loops */ + /* PyTracebackObject *cursor = (PyTracebackObject *)new_next; while (cursor) { if (cursor == self) { @@ -130,7 +132,7 @@ tb_next_set(PyTracebackObject *self, PyObject *new_next, void *Py_UNUSED(_)) return -1; } cursor = cursor->tb_next; - } + }*/ PyObject *old_next = (PyObject*)self->tb_next; Py_XINCREF(new_next); diff --git a/exception_group.py b/exception_group.py new file mode 100644 index 00000000000000..11786f31b27a2f --- /dev/null +++ b/exception_group.py @@ -0,0 +1,106 @@ +import sys +import traceback + +class Traceback: + def __init__(self, frame, tb = None): + self.tb_frame = frame + self.tb = tb + + +class TracebackGroup(Traceback): + def __init__(self, frame, tb_next_all = {}): + super().__init__(frame) + self.tb_next_all = tb_next_all + + def add(self, exc): + ''' add an exception to this tb group ''' + self.tb_next_all[exc] = exc.__traceback__ + + def split(self, excs): + ''' remove excs from this tb group and return a + new tb group for them, with same frame + ''' + r = dict({(k,v) for k,v in self.tb_next_all.items() if k in excs}) + [self.tb_next_all.pop(k) for k in r] + return TracebackGroup(self.tb_frame, r) + + +class ExceptionGroup(BaseException): + + def __init__(self, excs, tb=None): + self.excs = excs + if tb: + self.tb = tb + else: + self.tb = TracebackGroup(sys._getframe()) + for e in excs: + self.tb.add(e) + + def add_exc(self, e): + self.excs.add(e) + self.tb.add(e) + + def exc_match(self, E): + ''' remove the exceptions that match E + and return them in a new ExceptionGroup + ''' + matches = set() + for e in self.excs: + if isinstance(e, E): + matches.add(e) + [self.excs.remove(m) for m in matches] + tb = self.tb.split(matches) + return ExceptionGroup(matches, tb) + + def push_frame(self, frame): + self.__traceback__ = TracebackGroup(frame, tb_next=self.__traceback__) + + +def f(): raise ValueError('bad value: f') +def f1(): f() + +def g(): raise ValueError('bad value: g') +def g1(): g() + +def h(): raise TypeError('bad type: h') +def h1(): h() + +def aggregator(): + excs = set() + for c in (f1, g1, h1): + try: + c() + except Exception as e: + excs.add(e) + raise ExceptionGroup(excs) + +def propagator(): + aggregator() + +def propagator1(): + propagator() + +def handle_type_errors(): + try: + propagator1() + except ExceptionGroup as e: + TEs = e.exc_match(TypeError) + raise e + +def handle_value_errors(): + try: + propagator1() + except ExceptionGroup as e: + VEs = e.exc_match(ValueError) + raise e + + +def main(): + ## comment out the one you want to try: + + propagator1() + # handle_type_errors() + # handle_value_errors() + +if __name__ == '__main__': + main() From 7c9b194114153d1ce5da95fdb58f453f02bbae9f Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Sat, 24 Oct 2020 23:10:18 +0100 Subject: [PATCH 02/73] implemented traceback group in c - added map field to traceback so it can double up as a traceback group (avoids the need for subclassing it for the demo) --- Include/cpython/traceback.h | 1 + Objects/exceptions.c | 2 - Python/traceback.c | 131 ++++++++++++++++++++++++++++++++++-- exception_group.py | 112 +++++++++++++++++------------- 4 files changed, 192 insertions(+), 54 deletions(-) diff --git a/Include/cpython/traceback.h b/Include/cpython/traceback.h index aac5b42c344d3f..7cad6a9620141d 100644 --- a/Include/cpython/traceback.h +++ b/Include/cpython/traceback.h @@ -8,6 +8,7 @@ typedef struct _traceback { PyFrameObject *tb_frame; int tb_lasti; int tb_lineno; + PyObject *tb_next_map; } PyTracebackObject; PyAPI_FUNC(int) _Py_DisplaySourceLine(PyObject *, PyObject *, int, int); diff --git a/Objects/exceptions.c b/Objects/exceptions.c index 46c5e1371dfc69..4fe1a12d7cab41 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -227,13 +227,11 @@ BaseException_set_tb(PyBaseExceptionObject *self, PyObject *tb, void *Py_UNUSED( PyErr_SetString(PyExc_TypeError, "__traceback__ may not be deleted"); return -1; } - /* else if (!(tb == Py_None || PyTraceBack_Check(tb))) { PyErr_SetString(PyExc_TypeError, "__traceback__ must be a traceback or None"); return -1; } - */ Py_INCREF(tb); Py_XSETREF(self->traceback, tb); return 0; diff --git a/Python/traceback.c b/Python/traceback.c index 84a7720f17a341..ccfd7a17b519ca 100644 --- a/Python/traceback.c +++ b/Python/traceback.c @@ -51,6 +51,7 @@ tb_create_raw(PyTracebackObject *next, PyFrameObject *frame, int lasti, tb->tb_frame = frame; tb->tb_lasti = lasti; tb->tb_lineno = lineno; + tb->tb_next_map = NULL; PyObject_GC_Track(tb); } return (PyObject *)tb; @@ -68,6 +69,7 @@ TracebackType.__new__ as tb_new Create a new traceback object. [clinic start generated code]*/ + static PyObject * tb_new_impl(PyTypeObject *type, PyObject *tb_next, PyFrameObject *tb_frame, int tb_lasti, int tb_lineno) @@ -88,7 +90,7 @@ tb_new_impl(PyTypeObject *type, PyObject *tb_next, PyFrameObject *tb_frame, static PyObject * tb_dir(PyTracebackObject *self, PyObject *Py_UNUSED(ignored)) { - return Py_BuildValue("[ssss]", "tb_frame", "tb_next", + return Py_BuildValue("[sssss]", "tb_frame", "tb_next", "tb_next_map", "tb_lasti", "tb_lineno"); } @@ -110,21 +112,24 @@ tb_next_set(PyTracebackObject *self, PyObject *new_next, void *Py_UNUSED(_)) PyErr_Format(PyExc_TypeError, "can't delete tb_next attribute"); return -1; } + if (self->tb_next_map && new_next != Py_None) { + PyErr_Format(PyExc_ValueError, "can't have both tb_next and tb_next_map [2]"); + return -1; + } /* We accept None or a traceback object, and map None -> NULL (inverse of tb_next_get) */ if (new_next == Py_None) { new_next = NULL; - } /* - else if (!PyTraceBack_Check(new_next)) { + } else if (!PyTraceBack_Check(new_next)) { PyErr_Format(PyExc_TypeError, "expected traceback object, got '%s'", Py_TYPE(new_next)->tp_name); return -1; - }*/ + } /* Check for loops */ - /* + PyTracebackObject *cursor = (PyTracebackObject *)new_next; while (cursor) { if (cursor == self) { @@ -132,7 +137,7 @@ tb_next_set(PyTracebackObject *self, PyObject *new_next, void *Py_UNUSED(_)) return -1; } cursor = cursor->tb_next; - }*/ + } PyObject *old_next = (PyObject*)self->tb_next; Py_XINCREF(new_next); @@ -142,9 +147,119 @@ tb_next_set(PyTracebackObject *self, PyObject *new_next, void *Py_UNUSED(_)) return 0; } +static PyObject * +tb_next_map_get(PyTracebackObject *self, void *Py_UNUSED(_)) +{ + PyObject* ret = (PyObject*)self->tb_next_map; + if (!ret) { + ret = Py_None; + } + Py_INCREF(ret); + return ret; +} + + +static int +tb_next_map_add_impl(PyTracebackObject *self, PyObject *exc, PyTracebackObject *tb) { + + /* Check for loops */ +/* +// TODO: loop detection for map + PyTracebackObject *cursor = (PyTracebackObject *)new_next; + while (cursor) { + if (cursor == self) { + PyErr_Format(PyExc_ValueError, "traceback loop detected"); + return -1; + } + cursor = cursor->tb_next; + } +*/ + + if (!self->tb_next_map) { + if (self->tb_next != NULL && (PyObject*)self->tb_next != Py_None) { + PyErr_Format(PyExc_ValueError, "can't have both tb_next and tb_next_map [1]"); + return -1; + } + self->tb_next_map = PyDict_New(); + if (!self->tb_next_map) { + PyErr_Format(PyExc_ValueError, "dict create failed"); + return -1; + } + } + if (PyDict_SetItem(self->tb_next_map, exc, (PyObject*)tb) < 0) { + fprintf(stderr, "dict setitem failed"); + PyErr_Format(PyExc_ValueError, "dict setitem failed"); + return -1; + } + return 0; +} + +static PyObject * +tb_next_map_add(PyTracebackObject *self, PyObject *args) { + PyObject *exc; + PyObject *tb_; + if (!PyArg_ParseTuple(args, "OO", &exc, &tb_)) { + return NULL; + } + PyTracebackObject *tb = (PyTracebackObject *)tb_; + if (tb_next_map_add_impl(self, exc, tb) == -1) { + return NULL; + } + return Py_None; +} + +static PyTracebackObject * +tb_group_split(PyTracebackObject *self, PyObject *args) { + PyObject *excs; + if (!PyArg_ParseTuple(args, "O", &excs)) + return NULL; + + //remove excs from this tb group and return a + //new tb group for them, with same frame + if (self->tb_next) { + PyErr_Format(PyExc_TypeError, "not a traceback group"); + return NULL; + } + if (!PyList_Check(excs)) { + PyErr_Format(PyExc_TypeError, "excs not a list"); + return NULL; + } + + // TODO: special case where excs has size 1? + PyTracebackObject *result = (PyTracebackObject *)tb_create_raw(self->tb_next, self->tb_frame, self->tb_lasti, self->tb_lineno); + if (!result) { + PyErr_Format(PyExc_ValueError, "failed to create new traceback obj"); + return NULL; + } + + Py_ssize_t len = PyList_Size(excs); + for (Py_ssize_t i = 0; i < len; i++) { + PyObject *e = PyList_GET_ITEM(excs, i); + if (!e) { + return NULL; + } + PyObject *tb = PyDict_GetItem(self->tb_next_map, e); + if (!tb) { + PyErr_Format(PyExc_ValueError, "splitting on a non-existing exception"); + return NULL; + } + // remove e from self and add it to result + if (tb_next_map_add_impl(result, e, (PyTracebackObject *)tb) != 0) { + PyErr_Format(PyExc_TypeError, "failed to add exception to new traceback"); + return NULL; + } + if (PyDict_DelItem(self->tb_next_map, e) != 0) { + PyErr_Format(PyExc_TypeError, "failed to remove item in split"); + return NULL; + } + } + return result; +} static PyMethodDef tb_methods[] = { {"__dir__", (PyCFunction)tb_dir, METH_NOARGS}, + {"next_map_add", (PyCFunction)tb_next_map_add, METH_VARARGS}, + {"group_split", (PyCFunction)tb_group_split, METH_VARARGS}, {NULL, NULL, 0, NULL}, }; @@ -157,6 +272,7 @@ static PyMemberDef tb_memberlist[] = { static PyGetSetDef tb_getsetters[] = { {"tb_next", (getter)tb_next_get, (setter)tb_next_set, NULL, NULL}, + {"tb_next_map", (getter)tb_next_map_get, NULL, NULL, NULL}, {NULL} /* Sentinel */ }; @@ -167,6 +283,7 @@ tb_dealloc(PyTracebackObject *tb) Py_TRASHCAN_BEGIN(tb, tb_dealloc) Py_XDECREF(tb->tb_next); Py_XDECREF(tb->tb_frame); + Py_XDECREF(tb->tb_next_map); PyObject_GC_Del(tb); Py_TRASHCAN_END } @@ -176,6 +293,7 @@ tb_traverse(PyTracebackObject *tb, visitproc visit, void *arg) { Py_VISIT(tb->tb_next); Py_VISIT(tb->tb_frame); + Py_VISIT(tb->tb_next_map); return 0; } @@ -184,6 +302,7 @@ tb_clear(PyTracebackObject *tb) { Py_CLEAR(tb->tb_next); Py_CLEAR(tb->tb_frame); + Py_CLEAR(tb->tb_next_map); return 0; } diff --git a/exception_group.py b/exception_group.py index 11786f31b27a2f..9b513fc41b3a52 100644 --- a/exception_group.py +++ b/exception_group.py @@ -1,59 +1,63 @@ import sys import traceback - -class Traceback: - def __init__(self, frame, tb = None): - self.tb_frame = frame - self.tb = tb - - -class TracebackGroup(Traceback): - def __init__(self, frame, tb_next_all = {}): - super().__init__(frame) - self.tb_next_all = tb_next_all - - def add(self, exc): - ''' add an exception to this tb group ''' - self.tb_next_all[exc] = exc.__traceback__ - - def split(self, excs): - ''' remove excs from this tb group and return a - new tb group for them, with same frame - ''' - r = dict({(k,v) for k,v in self.tb_next_all.items() if k in excs}) - [self.tb_next_all.pop(k) for k in r] - return TracebackGroup(self.tb_frame, r) +import types class ExceptionGroup(BaseException): def __init__(self, excs, tb=None): - self.excs = excs + self.excs = set(excs) if tb: - self.tb = tb + self.__traceback__ = tb else: - self.tb = TracebackGroup(sys._getframe()) + self.__traceback__ = types.TracebackType(None, sys._getframe(), 0, 0) for e in excs: - self.tb.add(e) + self.add_exc(e) def add_exc(self, e): self.excs.add(e) - self.tb.add(e) + self.__traceback__.next_map_add(e, e.__traceback__) - def exc_match(self, E): + def split(self, E): ''' remove the exceptions that match E and return them in a new ExceptionGroup ''' - matches = set() + matches = [] for e in self.excs: if isinstance(e, E): - matches.add(e) + matches.append(e) [self.excs.remove(m) for m in matches] - tb = self.tb.split(matches) + gtb = self.__traceback__ + while gtb.tb_next: # there could be normal tbs is the ExceptionGroup propagated + gtb = gtb.tb_next + tb = gtb.group_split(matches) + return ExceptionGroup(matches, tb) def push_frame(self, frame): - self.__traceback__ = TracebackGroup(frame, tb_next=self.__traceback__) + self.__traceback__ = types.TracebackType(self.__traceback__, frame, 0, 0) + + def __str__(self): + return f"ExceptionGroup({self.excs})" + + def __repr__(self): + return str(self) + +def render_exception(exc, tb=None, indent=0): + print(exc) + tb = tb or exc.__traceback__ + while tb: + print(' '*indent, tb.tb_frame) + if tb.tb_next: # single traceback + tb = tb.tb_next + elif tb.tb_next_map: + indent += 4 + for e, t in tb.tb_next_map.items(): + print('---------------------------------------') + render_exception(e, t, indent) + tb = None + else: + tb = None def f(): raise ValueError('bad value: f') @@ -77,30 +81,46 @@ def aggregator(): def propagator(): aggregator() -def propagator1(): - propagator() +def get_exception_group(): + try: + propagator() + except ExceptionGroup as e: + return e def handle_type_errors(): try: - propagator1() + propagator() except ExceptionGroup as e: - TEs = e.exc_match(TypeError) - raise e + TEs = e.split(TypeError) + return e, TEs def handle_value_errors(): try: - propagator1() + propagator() except ExceptionGroup as e: - VEs = e.exc_match(ValueError) - raise e + VEs = e.split(ValueError) + return e, VEs def main(): - ## comment out the one you want to try: - - propagator1() - # handle_type_errors() - # handle_value_errors() + print (">>>>>>>>>>>>>>>>>> get_exception_group <<<<<<<<<<<<<<<<<<<<") + e = get_exception_group() + render_exception(e) + + print (">>>>>>>>>>>>>>>>>> handle_type_errors <<<<<<<<<<<<<<<<<<<<") + + e, TEs = handle_type_errors() + print ("\n\n\n ------------- The split-off Type Errors:") + render_exception(TEs) + print ("\n\n\n ------------- The remaining unhandled:") + render_exception(e) + + print (">>>>>>>>>>>>>>>>>> handle_value_errors <<<<<<<<<<<<<<<<<<<<") + e, VEs = handle_value_errors() + print ("\n\n\n ------------- The split-off Value Errors:") + render_exception(VEs) + print ("\n\n\n ------------- The remaining unhandled:") + render_exception(e) if __name__ == '__main__': main() From 030818dc10c9811faf32587b54d47ac7065d435e Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Sat, 24 Oct 2020 23:28:31 +0100 Subject: [PATCH 03/73] add output of exception_group.py --- output.txt | 84 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 output.txt diff --git a/output.txt b/output.txt new file mode 100644 index 00000000000000..9942fa46589918 --- /dev/null +++ b/output.txt @@ -0,0 +1,84 @@ +Running Release|Win32 interpreter... +>>>>>>>>>>>>>>>>>> get_exception_group <<<<<<<<<<<<<<<<<<<< +ExceptionGroup({ValueError('bad value: f'), ValueError('bad value: g'), TypeError('bad type: h')}) + + + + +--------------------------------------- +bad value: f + + + +--------------------------------------- +bad value: g + + + +--------------------------------------- +bad type: h + + + +>>>>>>>>>>>>>>>>>> handle_type_errors <<<<<<<<<<<<<<<<<<<< + + + + ------------- The split-off Type Errors: +ExceptionGroup({TypeError('bad type: h')}) + +--------------------------------------- +bad type: h + + + + + + + ------------- The remaining unhandled: +ExceptionGroup({ValueError('bad value: f'), ValueError('bad value: g')}) + + + + +--------------------------------------- +bad value: f + + + +--------------------------------------- +bad value: g + + + +>>>>>>>>>>>>>>>>>> handle_value_errors <<<<<<<<<<<<<<<<<<<< + + + + ------------- The split-off Value Errors: +ExceptionGroup({ValueError('bad value: f'), ValueError('bad value: g')}) + +--------------------------------------- +bad value: f + + + +--------------------------------------- +bad value: g + + + + + + + ------------- The remaining unhandled: +ExceptionGroup({TypeError('bad type: h')}) + + + + +--------------------------------------- +bad type: h + + + From 3d01e30d74f4e8c644e6f7e6204bebf8fe7bd3d5 Mon Sep 17 00:00:00 2001 From: Yury Selivanov Date: Sat, 24 Oct 2020 17:26:53 -0700 Subject: [PATCH 04/73] Move ExceptionGroup to types; add asyncio.TaskGroup --- Lib/asyncio/__init__.py | 4 +- Lib/asyncio/taskgroup.py | 282 +++++++++++++++++++++++++++++++++++++++ Lib/types.py | 64 ++++++++- exception_group.py | 75 ++--------- tg1.py | 37 +++++ 5 files changed, 394 insertions(+), 68 deletions(-) create mode 100644 Lib/asyncio/taskgroup.py create mode 100644 tg1.py diff --git a/Lib/asyncio/__init__.py b/Lib/asyncio/__init__.py index eb84bfb189ccf3..7501179d9be75d 100644 --- a/Lib/asyncio/__init__.py +++ b/Lib/asyncio/__init__.py @@ -19,6 +19,7 @@ from .tasks import * from .threads import * from .transports import * +from .taskgroup import * # Exposed for _asynciomodule.c to implement now deprecated # Task.all_tasks() method. This function will be removed in 3.9. @@ -37,7 +38,8 @@ subprocess.__all__ + tasks.__all__ + threads.__all__ + - transports.__all__) + transports.__all__ + + taskgroup.__all__) if sys.platform == 'win32': # pragma: no cover from .windows_events import * diff --git a/Lib/asyncio/taskgroup.py b/Lib/asyncio/taskgroup.py new file mode 100644 index 00000000000000..55e8bc2c67e0f6 --- /dev/null +++ b/Lib/asyncio/taskgroup.py @@ -0,0 +1,282 @@ +# +# This source file is part of the EdgeDB open source project. +# +# Copyright 2016-present MagicStack Inc. and the EdgeDB authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + + +from __future__ import annotations + +import asyncio +import functools +import itertools +import sys +import types + +__all__ = ('TaskGroup',) + + +class TaskGroup: + + def __init__(self, *, name=None): + if name is None: + self._name = f'tg-{_name_counter()}' + else: + self._name = str(name) + + self._entered = False + self._exiting = False + self._aborting = False + self._loop = None + self._parent_task = None + self._parent_cancel_requested = False + self._tasks = set() + self._unfinished_tasks = 0 + self._errors = [] + self._base_error = None + self._on_completed_fut = None + + def get_name(self): + return self._name + + def __repr__(self): + msg = f'= (3, 8): + + # In Python 3.8 Tasks propagate all exceptions correctly, + # except for KeyboardInterrupt and SystemExit which are + # still considered special. + + def _is_base_error(self, exc: BaseException) -> bool: + assert isinstance(exc, BaseException) + return isinstance(exc, (SystemExit, KeyboardInterrupt)) + + else: + + # In Python prior to 3.8 all BaseExceptions are special and + # are bypassing the proper propagation through async/await + # code, essentially aborting the execution. + + def _is_base_error(self, exc: BaseException) -> bool: + assert isinstance(exc, BaseException) + return not isinstance(exc, Exception) + + def _patch_task(self, task): + # In Python 3.8 we'll need proper API on asyncio.Task to + # make TaskGroups possible. We need to be able to access + # information about task cancellation, more specifically, + # we need a flag to say if a task was cancelled or not. + # We also need to be able to flip that flag. + + def _task_cancel(task, orig_cancel): + task.__cancel_requested__ = True + return orig_cancel() + + if hasattr(task, '__cancel_requested__'): + return + + task.__cancel_requested__ = False + # confirm that we were successful at adding the new attribute: + assert not task.__cancel_requested__ + + orig_cancel = task.cancel + task.cancel = functools.partial(_task_cancel, task, orig_cancel) + + def _abort(self): + self._aborting = True + + for t in self._tasks: + if not t.done(): + t.cancel() + + def _on_task_done(self, task): + self._unfinished_tasks -= 1 + assert self._unfinished_tasks >= 0 + + if self._exiting and not self._unfinished_tasks: + if not self._on_completed_fut.done(): + self._on_completed_fut.set_result(True) + + if task.cancelled(): + return + + exc = task.exception() + if exc is None: + return + + self._errors.append(exc) + if self._is_base_error(exc) and self._base_error is None: + self._base_error = exc + + if self._parent_task.done(): + # Not sure if this case is possible, but we want to handle + # it anyways. + self._loop.call_exception_handler({ + 'message': f'Task {task!r} has errored out but its parent ' + f'task {self._parent_task} is already completed', + 'exception': exc, + 'task': task, + }) + return + + self._abort() + if not self._parent_task.__cancel_requested__: + # If parent task *is not* being cancelled, it means that we want + # to manually cancel it to abort whatever is being run right now + # in the TaskGroup. But we want to mark parent task as + # "not cancelled" later in __aexit__. Example situation that + # we need to handle: + # + # async def foo(): + # try: + # async with TaskGroup() as g: + # g.create_task(crash_soon()) + # await something # <- this needs to be canceled + # # by the TaskGroup, e.g. + # # foo() needs to be cancelled + # except Exception: + # # Ignore any exceptions raised in the TaskGroup + # pass + # await something_else # this line has to be called + # # after TaskGroup is finished. + self._parent_cancel_requested = True + self._parent_task.cancel() + +_name_counter = itertools.count(1).__next__ diff --git a/Lib/types.py b/Lib/types.py index 532f4806fc0226..95755fba4419d4 100644 --- a/Lib/types.py +++ b/Lib/types.py @@ -62,7 +62,7 @@ def _m(self): pass GetSetDescriptorType = type(FunctionType.__code__) MemberDescriptorType = type(FunctionType.__globals__) -del sys, _f, _g, _C, _c, _ag # Not for export +del _f, _g, _C, _c, _ag # Not for export # Provide a PEP 3115 compliant mechanism for class creation @@ -300,4 +300,66 @@ def wrapped(*args, **kwargs): NoneType = type(None) NotImplementedType = type(NotImplemented) +class ExceptionGroup(BaseException): + + def __init__(self, excs, tb=None): + self.excs = set(excs) + if tb: + self.__traceback__ = tb + else: + import types + self.__traceback__ = types.TracebackType( + None, sys._getframe(), 0, 0) + for e in excs: + self.add_exc(e) + + def add_exc(self, e): + self.excs.add(e) + self.__traceback__.next_map_add(e, e.__traceback__) + + def split(self, E): + ''' remove the exceptions that match E + and return them in a new ExceptionGroup + ''' + matches = [] + for e in self.excs: + if isinstance(e, E): + matches.append(e) + [self.excs.remove(m) for m in matches] + gtb = self.__traceback__ + while gtb.tb_next: + # there could be normal tbs is the ExceptionGroup propagated + gtb = gtb.tb_next + tb = gtb.group_split(matches) + + return ExceptionGroup(matches, tb) + + def push_frame(self, frame): + import types + self.__traceback__ = types.TracebackType( + self.__traceback__, frame, 0, 0) + + @staticmethod + def render(exc, tb=None, indent=0): + print(exc) + tb = tb or exc.__traceback__ + while tb: + print(' '*indent, tb.tb_frame) + if tb.tb_next: # single traceback + tb = tb.tb_next + elif tb.tb_next_map: + indent += 4 + for e, t in tb.tb_next_map.items(): + print('---------------------------------------') + ExceptionGroup.render(e, t, indent) + tb = None + else: + tb = None + + def __str__(self): + return f"ExceptionGroup({self.excs})" + + def __repr__(self): + return str(self) + __all__ = [n for n in globals() if n[:1] != '_'] diff --git a/exception_group.py b/exception_group.py index 9b513fc41b3a52..2c9de8f5cf7415 100644 --- a/exception_group.py +++ b/exception_group.py @@ -3,63 +3,6 @@ import types -class ExceptionGroup(BaseException): - - def __init__(self, excs, tb=None): - self.excs = set(excs) - if tb: - self.__traceback__ = tb - else: - self.__traceback__ = types.TracebackType(None, sys._getframe(), 0, 0) - for e in excs: - self.add_exc(e) - - def add_exc(self, e): - self.excs.add(e) - self.__traceback__.next_map_add(e, e.__traceback__) - - def split(self, E): - ''' remove the exceptions that match E - and return them in a new ExceptionGroup - ''' - matches = [] - for e in self.excs: - if isinstance(e, E): - matches.append(e) - [self.excs.remove(m) for m in matches] - gtb = self.__traceback__ - while gtb.tb_next: # there could be normal tbs is the ExceptionGroup propagated - gtb = gtb.tb_next - tb = gtb.group_split(matches) - - return ExceptionGroup(matches, tb) - - def push_frame(self, frame): - self.__traceback__ = types.TracebackType(self.__traceback__, frame, 0, 0) - - def __str__(self): - return f"ExceptionGroup({self.excs})" - - def __repr__(self): - return str(self) - -def render_exception(exc, tb=None, indent=0): - print(exc) - tb = tb or exc.__traceback__ - while tb: - print(' '*indent, tb.tb_frame) - if tb.tb_next: # single traceback - tb = tb.tb_next - elif tb.tb_next_map: - indent += 4 - for e, t in tb.tb_next_map.items(): - print('---------------------------------------') - render_exception(e, t, indent) - tb = None - else: - tb = None - - def f(): raise ValueError('bad value: f') def f1(): f() @@ -76,7 +19,7 @@ def aggregator(): c() except Exception as e: excs.add(e) - raise ExceptionGroup(excs) + raise types.ExceptionGroup(excs) def propagator(): aggregator() @@ -84,20 +27,20 @@ def propagator(): def get_exception_group(): try: propagator() - except ExceptionGroup as e: + except types.ExceptionGroup as e: return e def handle_type_errors(): try: propagator() - except ExceptionGroup as e: + except types.ExceptionGroup as e: TEs = e.split(TypeError) return e, TEs def handle_value_errors(): try: propagator() - except ExceptionGroup as e: + except types.ExceptionGroup as e: VEs = e.split(ValueError) return e, VEs @@ -105,22 +48,22 @@ def handle_value_errors(): def main(): print (">>>>>>>>>>>>>>>>>> get_exception_group <<<<<<<<<<<<<<<<<<<<") e = get_exception_group() - render_exception(e) + types.ExceptionGroup.render(e) print (">>>>>>>>>>>>>>>>>> handle_type_errors <<<<<<<<<<<<<<<<<<<<") e, TEs = handle_type_errors() print ("\n\n\n ------------- The split-off Type Errors:") - render_exception(TEs) + types.ExceptionGroup.render(TEs) print ("\n\n\n ------------- The remaining unhandled:") - render_exception(e) + types.ExceptionGroup.render(e) print (">>>>>>>>>>>>>>>>>> handle_value_errors <<<<<<<<<<<<<<<<<<<<") e, VEs = handle_value_errors() print ("\n\n\n ------------- The split-off Value Errors:") - render_exception(VEs) + types.ExceptionGroup.render(VEs) print ("\n\n\n ------------- The remaining unhandled:") - render_exception(e) + types.ExceptionGroup.render(e) if __name__ == '__main__': main() diff --git a/tg1.py b/tg1.py new file mode 100644 index 00000000000000..6bb00e75650847 --- /dev/null +++ b/tg1.py @@ -0,0 +1,37 @@ +import asyncio +import types + +async def t1(): + await asyncio.sleep(0.5) + 1 / 0 + +async def t2(): + async with asyncio.TaskGroup() as tg: + tg.create_task(t21()) + tg.create_task(t22()) + +async def t21(): + await asyncio.sleep(0.3) + raise ValueError + +async def t22(): + await asyncio.sleep(0.7) + raise TypeError + +async def main(): + async with asyncio.TaskGroup() as tg: + tg.create_task(t1()) + tg.create_task(t2()) + + +def run(*args): + try: + asyncio.run(*args) + except types.ExceptionGroup as e: + print('============') + types.ExceptionGroup.render(e) + print('^^^^^^^^^^^^') + raise + + +run(main()) From f9ccf2230262bbe1be66c033fc7477a15b8ea980 Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Wed, 28 Oct 2020 11:14:01 +0000 Subject: [PATCH 05/73] remove add_exc from ExceptionGroup --- Lib/types.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/Lib/types.py b/Lib/types.py index 95755fba4419d4..1c452ca2a3b920 100644 --- a/Lib/types.py +++ b/Lib/types.py @@ -311,11 +311,8 @@ def __init__(self, excs, tb=None): self.__traceback__ = types.TracebackType( None, sys._getframe(), 0, 0) for e in excs: - self.add_exc(e) - - def add_exc(self, e): - self.excs.add(e) - self.__traceback__.next_map_add(e, e.__traceback__) + self.excs.add(e) + self.__traceback__.next_map_add(e, e.__traceback__) def split(self, E): ''' remove the exceptions that match E From 404ab68f737c9e60e12717bf6558c01de6cbb20e Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Wed, 28 Oct 2020 13:19:46 +0000 Subject: [PATCH 06/73] pure-python, immutable ExceptionGroup --- Lib/types.py | 78 ++++++++++++++++++++++++---------------- exception_group.py | 16 ++++----- output.txt | 88 +++++++++++++++++++++------------------------- 3 files changed, 97 insertions(+), 85 deletions(-) diff --git a/Lib/types.py b/Lib/types.py index 1c452ca2a3b920..07636d19f52682 100644 --- a/Lib/types.py +++ b/Lib/types.py @@ -300,36 +300,47 @@ def wrapped(*args, **kwargs): NoneType = type(None) NotImplementedType = type(NotImplemented) +class TracebackGroup: + def __init__(self, excs, frame): + self.tb_frame = frame + self.tb_next_map = {} # TODO: make it a weak key dict + for e in excs: + self.tb_next_map[e] = e.__traceback__ + class ExceptionGroup(BaseException): - def __init__(self, excs, tb=None): + def __init__(self, excs, frame=None): self.excs = set(excs) - if tb: - self.__traceback__ = tb - else: - import types - self.__traceback__ = types.TracebackType( - None, sys._getframe(), 0, 0) - for e in excs: - self.excs.add(e) - self.__traceback__.next_map_add(e, e.__traceback__) + self.frame = frame or sys._getframe() + # self.__traceback__ is updated as usual, but self.__traceback_group__ + # is the frame where the exception group was created (and it is + # preserved on splits). So __traceback_group__ + __traceback__ + # gives us the full path. + import types + self.__traceback__ = types.TracebackType(None, self.frame, 0, 0) + self.__traceback_group__ = TracebackGroup(self.excs, self.frame) def split(self, E): - ''' remove the exceptions that match E - and return them in a new ExceptionGroup + ''' returns two new ExceptionGroups: match, rest + of the exceptions of self that match E and those + that don't. + match and rest have the same nested structure as self. + E can be a type or tuple of types. ''' - matches = [] + match, rest = [], [] for e in self.excs: - if isinstance(e, E): - matches.append(e) - [self.excs.remove(m) for m in matches] - gtb = self.__traceback__ - while gtb.tb_next: - # there could be normal tbs is the ExceptionGroup propagated - gtb = gtb.tb_next - tb = gtb.group_split(matches) - - return ExceptionGroup(matches, tb) + if isinstance(e, ExceptionGroup): # recurse + e_match, e_rest = e.split(E) + match.append(e_match) + rest.append(e_rest) + else: + if isinstance(e, E): + match.append(e) + e_match, e_rest = e, None + else: + rest.append(e) + frame = self.frame + return ExceptionGroup(match, frame),ExceptionGroup(rest, frame) def push_frame(self, frame): import types @@ -339,18 +350,25 @@ def push_frame(self, frame): @staticmethod def render(exc, tb=None, indent=0): print(exc) - tb = tb or exc.__traceback__ + try: + tb = tb or exc.__traceback__ + except Exception as e: + import pdb; pdb.set_trace() + print(e) while tb: print(' '*indent, tb.tb_frame) if tb.tb_next: # single traceback tb = tb.tb_next - elif tb.tb_next_map: - indent += 4 - for e, t in tb.tb_next_map.items(): - print('---------------------------------------') - ExceptionGroup.render(e, t, indent) - tb = None else: + # if this is an ExceptioGroup, follow + # __traceback_group__ + if isinstance(exc, ExceptionGroup): + tbg = exc.__traceback_group__ + assert tbg + indent += 4 + for e, t in tbg.tb_next_map.items(): + print('---------------------------------------') + ExceptionGroup.render(e, t, indent) tb = None def __str__(self): diff --git a/exception_group.py b/exception_group.py index 2c9de8f5cf7415..7375f8319b4ee2 100644 --- a/exception_group.py +++ b/exception_group.py @@ -34,15 +34,15 @@ def handle_type_errors(): try: propagator() except types.ExceptionGroup as e: - TEs = e.split(TypeError) - return e, TEs + TEs, rest = e.split(TypeError) + return TEs, rest def handle_value_errors(): try: propagator() except types.ExceptionGroup as e: - VEs = e.split(ValueError) - return e, VEs + VEs, rest = e.split(ValueError) + return VEs, rest def main(): @@ -52,18 +52,18 @@ def main(): print (">>>>>>>>>>>>>>>>>> handle_type_errors <<<<<<<<<<<<<<<<<<<<") - e, TEs = handle_type_errors() + TEs, rest = handle_type_errors() print ("\n\n\n ------------- The split-off Type Errors:") types.ExceptionGroup.render(TEs) print ("\n\n\n ------------- The remaining unhandled:") - types.ExceptionGroup.render(e) + types.ExceptionGroup.render(rest) print (">>>>>>>>>>>>>>>>>> handle_value_errors <<<<<<<<<<<<<<<<<<<<") - e, VEs = handle_value_errors() + VEs, rest = handle_value_errors() print ("\n\n\n ------------- The split-off Value Errors:") types.ExceptionGroup.render(VEs) print ("\n\n\n ------------- The remaining unhandled:") - types.ExceptionGroup.render(e) + types.ExceptionGroup.render(rest) if __name__ == '__main__': main() diff --git a/output.txt b/output.txt index 9942fa46589918..da90fee7388770 100644 --- a/output.txt +++ b/output.txt @@ -1,84 +1,78 @@ Running Release|Win32 interpreter... >>>>>>>>>>>>>>>>>> get_exception_group <<<<<<<<<<<<<<<<<<<< -ExceptionGroup({ValueError('bad value: f'), ValueError('bad value: g'), TypeError('bad type: h')}) - - - - ---------------------------------------- -bad value: f - - - +ExceptionGroup({ValueError('bad value: g'), ValueError('bad value: f'), TypeError('bad type: h')}) + + + + --------------------------------------- bad value: g - - - + + + +--------------------------------------- +bad value: f + + + --------------------------------------- bad type: h - - - + + + >>>>>>>>>>>>>>>>>> handle_type_errors <<<<<<<<<<<<<<<<<<<< ------------- The split-off Type Errors: ExceptionGroup({TypeError('bad type: h')}) - + --------------------------------------- bad type: h - - - + + + ------------- The remaining unhandled: ExceptionGroup({ValueError('bad value: f'), ValueError('bad value: g')}) - - - - + --------------------------------------- bad value: f - - - + + + --------------------------------------- bad value: g - - - + + + >>>>>>>>>>>>>>>>>> handle_value_errors <<<<<<<<<<<<<<<<<<<< ------------- The split-off Value Errors: -ExceptionGroup({ValueError('bad value: f'), ValueError('bad value: g')}) - ---------------------------------------- -bad value: f - - - +ExceptionGroup({ValueError('bad value: g'), ValueError('bad value: f')}) + --------------------------------------- bad value: g - - - + + + +--------------------------------------- +bad value: f + + + ------------- The remaining unhandled: ExceptionGroup({TypeError('bad type: h')}) - - - - + --------------------------------------- bad type: h - - - + + + From 3995d2f2b5cbbdd75d0b6a4722c0b5992c082e76 Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Wed, 28 Oct 2020 14:38:48 +0000 Subject: [PATCH 07/73] revert c changes --- Include/cpython/traceback.h | 1 - Objects/exceptions.c | 1 + Python/traceback.c | 122 +----------------------------------- 3 files changed, 2 insertions(+), 122 deletions(-) diff --git a/Include/cpython/traceback.h b/Include/cpython/traceback.h index 7cad6a9620141d..aac5b42c344d3f 100644 --- a/Include/cpython/traceback.h +++ b/Include/cpython/traceback.h @@ -8,7 +8,6 @@ typedef struct _traceback { PyFrameObject *tb_frame; int tb_lasti; int tb_lineno; - PyObject *tb_next_map; } PyTracebackObject; PyAPI_FUNC(int) _Py_DisplaySourceLine(PyObject *, PyObject *, int, int); diff --git a/Objects/exceptions.c b/Objects/exceptions.c index 4fe1a12d7cab41..d4824938a0f507 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -232,6 +232,7 @@ BaseException_set_tb(PyBaseExceptionObject *self, PyObject *tb, void *Py_UNUSED( "__traceback__ must be a traceback or None"); return -1; } + Py_INCREF(tb); Py_XSETREF(self->traceback, tb); return 0; diff --git a/Python/traceback.c b/Python/traceback.c index ccfd7a17b519ca..11eb2f268256a9 100644 --- a/Python/traceback.c +++ b/Python/traceback.c @@ -51,7 +51,6 @@ tb_create_raw(PyTracebackObject *next, PyFrameObject *frame, int lasti, tb->tb_frame = frame; tb->tb_lasti = lasti; tb->tb_lineno = lineno; - tb->tb_next_map = NULL; PyObject_GC_Track(tb); } return (PyObject *)tb; @@ -69,7 +68,6 @@ TracebackType.__new__ as tb_new Create a new traceback object. [clinic start generated code]*/ - static PyObject * tb_new_impl(PyTypeObject *type, PyObject *tb_next, PyFrameObject *tb_frame, int tb_lasti, int tb_lineno) @@ -90,7 +88,7 @@ tb_new_impl(PyTypeObject *type, PyObject *tb_next, PyFrameObject *tb_frame, static PyObject * tb_dir(PyTracebackObject *self, PyObject *Py_UNUSED(ignored)) { - return Py_BuildValue("[sssss]", "tb_frame", "tb_next", "tb_next_map", + return Py_BuildValue("[ssss]", "tb_frame", "tb_next", "tb_lasti", "tb_lineno"); } @@ -112,10 +110,6 @@ tb_next_set(PyTracebackObject *self, PyObject *new_next, void *Py_UNUSED(_)) PyErr_Format(PyExc_TypeError, "can't delete tb_next attribute"); return -1; } - if (self->tb_next_map && new_next != Py_None) { - PyErr_Format(PyExc_ValueError, "can't have both tb_next and tb_next_map [2]"); - return -1; - } /* We accept None or a traceback object, and map None -> NULL (inverse of tb_next_get) */ @@ -129,7 +123,6 @@ tb_next_set(PyTracebackObject *self, PyObject *new_next, void *Py_UNUSED(_)) } /* Check for loops */ - PyTracebackObject *cursor = (PyTracebackObject *)new_next; while (cursor) { if (cursor == self) { @@ -147,119 +140,10 @@ tb_next_set(PyTracebackObject *self, PyObject *new_next, void *Py_UNUSED(_)) return 0; } -static PyObject * -tb_next_map_get(PyTracebackObject *self, void *Py_UNUSED(_)) -{ - PyObject* ret = (PyObject*)self->tb_next_map; - if (!ret) { - ret = Py_None; - } - Py_INCREF(ret); - return ret; -} - - -static int -tb_next_map_add_impl(PyTracebackObject *self, PyObject *exc, PyTracebackObject *tb) { - - /* Check for loops */ -/* -// TODO: loop detection for map - PyTracebackObject *cursor = (PyTracebackObject *)new_next; - while (cursor) { - if (cursor == self) { - PyErr_Format(PyExc_ValueError, "traceback loop detected"); - return -1; - } - cursor = cursor->tb_next; - } -*/ - if (!self->tb_next_map) { - if (self->tb_next != NULL && (PyObject*)self->tb_next != Py_None) { - PyErr_Format(PyExc_ValueError, "can't have both tb_next and tb_next_map [1]"); - return -1; - } - self->tb_next_map = PyDict_New(); - if (!self->tb_next_map) { - PyErr_Format(PyExc_ValueError, "dict create failed"); - return -1; - } - } - if (PyDict_SetItem(self->tb_next_map, exc, (PyObject*)tb) < 0) { - fprintf(stderr, "dict setitem failed"); - PyErr_Format(PyExc_ValueError, "dict setitem failed"); - return -1; - } - return 0; -} - -static PyObject * -tb_next_map_add(PyTracebackObject *self, PyObject *args) { - PyObject *exc; - PyObject *tb_; - if (!PyArg_ParseTuple(args, "OO", &exc, &tb_)) { - return NULL; - } - PyTracebackObject *tb = (PyTracebackObject *)tb_; - if (tb_next_map_add_impl(self, exc, tb) == -1) { - return NULL; - } - return Py_None; -} - -static PyTracebackObject * -tb_group_split(PyTracebackObject *self, PyObject *args) { - PyObject *excs; - if (!PyArg_ParseTuple(args, "O", &excs)) - return NULL; - - //remove excs from this tb group and return a - //new tb group for them, with same frame - if (self->tb_next) { - PyErr_Format(PyExc_TypeError, "not a traceback group"); - return NULL; - } - if (!PyList_Check(excs)) { - PyErr_Format(PyExc_TypeError, "excs not a list"); - return NULL; - } - - // TODO: special case where excs has size 1? - PyTracebackObject *result = (PyTracebackObject *)tb_create_raw(self->tb_next, self->tb_frame, self->tb_lasti, self->tb_lineno); - if (!result) { - PyErr_Format(PyExc_ValueError, "failed to create new traceback obj"); - return NULL; - } - - Py_ssize_t len = PyList_Size(excs); - for (Py_ssize_t i = 0; i < len; i++) { - PyObject *e = PyList_GET_ITEM(excs, i); - if (!e) { - return NULL; - } - PyObject *tb = PyDict_GetItem(self->tb_next_map, e); - if (!tb) { - PyErr_Format(PyExc_ValueError, "splitting on a non-existing exception"); - return NULL; - } - // remove e from self and add it to result - if (tb_next_map_add_impl(result, e, (PyTracebackObject *)tb) != 0) { - PyErr_Format(PyExc_TypeError, "failed to add exception to new traceback"); - return NULL; - } - if (PyDict_DelItem(self->tb_next_map, e) != 0) { - PyErr_Format(PyExc_TypeError, "failed to remove item in split"); - return NULL; - } - } - return result; -} static PyMethodDef tb_methods[] = { {"__dir__", (PyCFunction)tb_dir, METH_NOARGS}, - {"next_map_add", (PyCFunction)tb_next_map_add, METH_VARARGS}, - {"group_split", (PyCFunction)tb_group_split, METH_VARARGS}, {NULL, NULL, 0, NULL}, }; @@ -272,7 +156,6 @@ static PyMemberDef tb_memberlist[] = { static PyGetSetDef tb_getsetters[] = { {"tb_next", (getter)tb_next_get, (setter)tb_next_set, NULL, NULL}, - {"tb_next_map", (getter)tb_next_map_get, NULL, NULL, NULL}, {NULL} /* Sentinel */ }; @@ -283,7 +166,6 @@ tb_dealloc(PyTracebackObject *tb) Py_TRASHCAN_BEGIN(tb, tb_dealloc) Py_XDECREF(tb->tb_next); Py_XDECREF(tb->tb_frame); - Py_XDECREF(tb->tb_next_map); PyObject_GC_Del(tb); Py_TRASHCAN_END } @@ -293,7 +175,6 @@ tb_traverse(PyTracebackObject *tb, visitproc visit, void *arg) { Py_VISIT(tb->tb_next); Py_VISIT(tb->tb_frame); - Py_VISIT(tb->tb_next_map); return 0; } @@ -302,7 +183,6 @@ tb_clear(PyTracebackObject *tb) { Py_CLEAR(tb->tb_next); Py_CLEAR(tb->tb_frame); - Py_CLEAR(tb->tb_next_map); return 0; } From 6c78891ca777ad51ac6d769d83d5d4aa619c6811 Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Wed, 28 Oct 2020 16:45:47 +0000 Subject: [PATCH 08/73] fixed merging of nested TBGs. Added __iter__ on EG. removed frame from TBG and EG - we take it from the __traceback__ which is set when the EG is raised --- Lib/types.py | 61 ++++++++++++++++------------- exception_group.py | 28 +++++++++----- output.txt | 96 ++++++++++------------------------------------ 3 files changed, 74 insertions(+), 111 deletions(-) diff --git a/Lib/types.py b/Lib/types.py index 07636d19f52682..8858e08b58217c 100644 --- a/Lib/types.py +++ b/Lib/types.py @@ -301,24 +301,29 @@ def wrapped(*args, **kwargs): NotImplementedType = type(NotImplemented) class TracebackGroup: - def __init__(self, excs, frame): - self.tb_frame = frame + def __init__(self, excs): self.tb_next_map = {} # TODO: make it a weak key dict + self._init_map(excs) + + def _init_map(self, excs): for e in excs: - self.tb_next_map[e] = e.__traceback__ + if isinstance(e, ExceptionGroup): + for e_ in e.excs: + self.tb_next_map[e_] = e_.__traceback__ + else: + self.tb_next_map[e] = e.__traceback__.tb_next class ExceptionGroup(BaseException): - def __init__(self, excs, frame=None): + def __init__(self, excs): self.excs = set(excs) - self.frame = frame or sys._getframe() # self.__traceback__ is updated as usual, but self.__traceback_group__ # is the frame where the exception group was created (and it is # preserved on splits). So __traceback_group__ + __traceback__ # gives us the full path. import types - self.__traceback__ = types.TracebackType(None, self.frame, 0, 0) - self.__traceback_group__ = TracebackGroup(self.excs, self.frame) + self.__traceback__ = None # will be set when the traceback group is raised + self.__traceback_group__ = TracebackGroup(self.excs) def split(self, E): ''' returns two new ExceptionGroups: match, rest @@ -347,29 +352,33 @@ def push_frame(self, frame): self.__traceback__ = types.TracebackType( self.__traceback__, frame, 0, 0) + @staticmethod + def _render_simple_tb(exc, tb=None, indent=0): + tb = tb or exc.__traceback__ + while tb and not isinstance(tb, TracebackGroup): + print('[0]',' '*indent, tb.tb_frame) + tb = tb.tb_next + @staticmethod def render(exc, tb=None, indent=0): print(exc) - try: - tb = tb or exc.__traceback__ - except Exception as e: - import pdb; pdb.set_trace() - print(e) - while tb: - print(' '*indent, tb.tb_frame) - if tb.tb_next: # single traceback - tb = tb.tb_next + ExceptionGroup._render_simple_tb(exc, tb, indent) + if isinstance(exc, ExceptionGroup): + tbg = exc.__traceback_group__ + assert isinstance(tbg, TracebackGroup) + indent += 4 + for e, t in tbg.tb_next_map.items(): + print('---------------------------------------') + ExceptionGroup.render(e, t, indent) + + def __iter__(self): + ''' iterate over the individual exceptions (flattens the tree) ''' + for e in self.excs: + if isinstance(e, ExceptionGroup): + for e_ in e: + yield e_ else: - # if this is an ExceptioGroup, follow - # __traceback_group__ - if isinstance(exc, ExceptionGroup): - tbg = exc.__traceback_group__ - assert tbg - indent += 4 - for e, t in tbg.tb_next_map.items(): - print('---------------------------------------') - ExceptionGroup.render(e, t, indent) - tb = None + yield e def __str__(self): return f"ExceptionGroup({self.excs})" diff --git a/exception_group.py b/exception_group.py index 7375f8319b4ee2..6333964f65cd6b 100644 --- a/exception_group.py +++ b/exception_group.py @@ -3,26 +3,36 @@ import types -def f(): raise ValueError('bad value: f') -def f1(): f() +def f(i=0): raise ValueError(f'bad value: f{i}') +def f1(): f(1) -def g(): raise ValueError('bad value: g') -def g1(): g() +def g(i=0): raise ValueError(f'bad value: g{i}') +def g1(): g(1) -def h(): raise TypeError('bad type: h') -def h1(): h() +def h(i=0): raise TypeError(f'bad type: h{i}') +def h1(): h(1) def aggregator(): excs = set() - for c in (f1, g1, h1): + for c in (f, g): try: c() except Exception as e: excs.add(e) raise types.ExceptionGroup(excs) +def aggregator1(): + excs = set() + for c in (f1, g1, aggregator): + try: + c() + except (Exception, types.ExceptionGroup) as e: + excs.add(e) + eg = types.ExceptionGroup(excs) + raise eg + def propagator(): - aggregator() + aggregator1() def get_exception_group(): try: @@ -49,7 +59,7 @@ def main(): print (">>>>>>>>>>>>>>>>>> get_exception_group <<<<<<<<<<<<<<<<<<<<") e = get_exception_group() types.ExceptionGroup.render(e) - + return print (">>>>>>>>>>>>>>>>>> handle_type_errors <<<<<<<<<<<<<<<<<<<<") TEs, rest = handle_type_errors() diff --git a/output.txt b/output.txt index da90fee7388770..7a99bd284e70bf 100644 --- a/output.txt +++ b/output.txt @@ -1,78 +1,22 @@ Running Release|Win32 interpreter... >>>>>>>>>>>>>>>>>> get_exception_group <<<<<<<<<<<<<<<<<<<< -ExceptionGroup({ValueError('bad value: g'), ValueError('bad value: f'), TypeError('bad type: h')}) - - - - ---------------------------------------- -bad value: g - - - ---------------------------------------- -bad value: f - - - ---------------------------------------- -bad type: h - - - ->>>>>>>>>>>>>>>>>> handle_type_errors <<<<<<<<<<<<<<<<<<<< - - - - ------------- The split-off Type Errors: -ExceptionGroup({TypeError('bad type: h')}) - ---------------------------------------- -bad type: h - - - - - - - ------------- The remaining unhandled: -ExceptionGroup({ValueError('bad value: f'), ValueError('bad value: g')}) - ---------------------------------------- -bad value: f - - - ---------------------------------------- -bad value: g - - - ->>>>>>>>>>>>>>>>>> handle_value_errors <<<<<<<<<<<<<<<<<<<< - - - - ------------- The split-off Value Errors: -ExceptionGroup({ValueError('bad value: g'), ValueError('bad value: f')}) - ---------------------------------------- -bad value: g - - - ---------------------------------------- -bad value: f - - - - - - - ------------- The remaining unhandled: -ExceptionGroup({TypeError('bad type: h')}) - ---------------------------------------- -bad type: h - - - +ExceptionGroup({ExceptionGroup({ValueError('bad value: g0'), ValueError('bad value: f0')}), ValueError('bad value: g1'), ValueError('bad value: f1')}) +[0] +[0] +[0] +--------------------------------------- +bad value: g0 +[0] +[0] +--------------------------------------- +bad value: f0 +[0] +[0] +--------------------------------------- +bad value: g1 +[0] +[0] +--------------------------------------- +bad value: f1 +[0] +[0] From c0c2833d7e8a587d656e87ddd1ae272fdef1f912 Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Wed, 28 Oct 2020 17:08:32 +0000 Subject: [PATCH 09/73] review comments from Guido. Also fixed split, and activated it in the test script --- Lib/types.py | 31 +++++++----------- exception_group.py | 2 +- output.txt | 80 ++++++++++++++++++++++++++++++++++++++-------- 3 files changed, 80 insertions(+), 33 deletions(-) diff --git a/Lib/types.py b/Lib/types.py index 8858e08b58217c..2bfa33a9cd88ec 100644 --- a/Lib/types.py +++ b/Lib/types.py @@ -318,20 +318,21 @@ class ExceptionGroup(BaseException): def __init__(self, excs): self.excs = set(excs) # self.__traceback__ is updated as usual, but self.__traceback_group__ - # is the frame where the exception group was created (and it is - # preserved on splits). So __traceback_group__ + __traceback__ + # is set when the exception group is created (and it is preserved on + # splits). So __traceback_group__ + __traceback__ # gives us the full path. import types self.__traceback__ = None # will be set when the traceback group is raised self.__traceback_group__ = TracebackGroup(self.excs) def split(self, E): - ''' returns two new ExceptionGroups: match, rest - of the exceptions of self that match E and those - that don't. + """Split an ExceptionGroup to extract exceptions matching E + + returns two new ExceptionGroups: match, rest of the exceptions of + self that match E and those that don't. match and rest have the same nested structure as self. E can be a type or tuple of types. - ''' + """ match, rest = [], [] for e in self.excs: if isinstance(e, ExceptionGroup): # recurse @@ -344,8 +345,7 @@ def split(self, E): e_match, e_rest = e, None else: rest.append(e) - frame = self.frame - return ExceptionGroup(match, frame),ExceptionGroup(rest, frame) + return ExceptionGroup(match),ExceptionGroup(rest) def push_frame(self, frame): import types @@ -353,16 +353,12 @@ def push_frame(self, frame): self.__traceback__, frame, 0, 0) @staticmethod - def _render_simple_tb(exc, tb=None, indent=0): + def render(exc, tb=None, indent=0): + print(exc) tb = tb or exc.__traceback__ while tb and not isinstance(tb, TracebackGroup): - print('[0]',' '*indent, tb.tb_frame) + print(' '*indent, tb.tb_frame) tb = tb.tb_next - - @staticmethod - def render(exc, tb=None, indent=0): - print(exc) - ExceptionGroup._render_simple_tb(exc, tb, indent) if isinstance(exc, ExceptionGroup): tbg = exc.__traceback_group__ assert isinstance(tbg, TracebackGroup) @@ -380,10 +376,7 @@ def __iter__(self): else: yield e - def __str__(self): - return f"ExceptionGroup({self.excs})" - def __repr__(self): - return str(self) + return f"ExceptionGroup({self.excs})" __all__ = [n for n in globals() if n[:1] != '_'] diff --git a/exception_group.py b/exception_group.py index 6333964f65cd6b..c6fb0187632430 100644 --- a/exception_group.py +++ b/exception_group.py @@ -59,7 +59,7 @@ def main(): print (">>>>>>>>>>>>>>>>>> get_exception_group <<<<<<<<<<<<<<<<<<<<") e = get_exception_group() types.ExceptionGroup.render(e) - return + print (">>>>>>>>>>>>>>>>>> handle_type_errors <<<<<<<<<<<<<<<<<<<<") TEs, rest = handle_type_errors() diff --git a/output.txt b/output.txt index 7a99bd284e70bf..d49caec3559f47 100644 --- a/output.txt +++ b/output.txt @@ -1,22 +1,76 @@ Running Release|Win32 interpreter... >>>>>>>>>>>>>>>>>> get_exception_group <<<<<<<<<<<<<<<<<<<< -ExceptionGroup({ExceptionGroup({ValueError('bad value: g0'), ValueError('bad value: f0')}), ValueError('bad value: g1'), ValueError('bad value: f1')}) -[0] -[0] -[0] +{ExceptionGroup({ValueError('bad value: f0'), ValueError('bad value: g0')}), ValueError('bad value: g1'), ValueError('bad value: f1')} + + + +--------------------------------------- +bad value: f0 + + --------------------------------------- bad value: g0 -[0] -[0] + + --------------------------------------- -bad value: f0 -[0] -[0] +bad value: g1 + + +--------------------------------------- +bad value: f1 + + +>>>>>>>>>>>>>>>>>> handle_type_errors <<<<<<<<<<<<<<<<<<<< + + + + ------------- The split-off Type Errors: +[ExceptionGroup(set())] + + + + ------------- The remaining unhandled: +[ExceptionGroup({ValueError('bad value: f0'), ValueError('bad value: g0')}), ValueError('bad value: f1'), ValueError('bad value: g1')] --------------------------------------- bad value: g1 -[0] -[0] + + +--------------------------------------- +bad value: f0 + + +--------------------------------------- +bad value: g0 + + --------------------------------------- bad value: f1 -[0] -[0] + + +>>>>>>>>>>>>>>>>>> handle_value_errors <<<<<<<<<<<<<<<<<<<< + + + + ------------- The split-off Value Errors: +[ValueError('bad value: f1'), ExceptionGroup({ValueError('bad value: g0'), ValueError('bad value: f0')}), ValueError('bad value: g1')] +--------------------------------------- +bad value: g0 + + +--------------------------------------- +bad value: f0 + + +--------------------------------------- +bad value: f1 + + +--------------------------------------- +bad value: g1 + + + + + + ------------- The remaining unhandled: +[ExceptionGroup(set())] From 3ccb9217873c2630d75e52541c71021d6c0521c1 Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Wed, 28 Oct 2020 17:24:46 +0000 Subject: [PATCH 10/73] move ExceptionGroup and TracebackGroup from types to their own script in Lib --- Lib/types.py | 79 ----------------------------- exception_group.py | 79 ----------------------------- exception_group_test.py | 79 +++++++++++++++++++++++++++++ output.txt | 110 ++++++++++++++++++++++++---------------- 4 files changed, 146 insertions(+), 201 deletions(-) delete mode 100644 exception_group.py create mode 100644 exception_group_test.py diff --git a/Lib/types.py b/Lib/types.py index 2bfa33a9cd88ec..3acf6808324e61 100644 --- a/Lib/types.py +++ b/Lib/types.py @@ -300,83 +300,4 @@ def wrapped(*args, **kwargs): NoneType = type(None) NotImplementedType = type(NotImplemented) -class TracebackGroup: - def __init__(self, excs): - self.tb_next_map = {} # TODO: make it a weak key dict - self._init_map(excs) - - def _init_map(self, excs): - for e in excs: - if isinstance(e, ExceptionGroup): - for e_ in e.excs: - self.tb_next_map[e_] = e_.__traceback__ - else: - self.tb_next_map[e] = e.__traceback__.tb_next - -class ExceptionGroup(BaseException): - - def __init__(self, excs): - self.excs = set(excs) - # self.__traceback__ is updated as usual, but self.__traceback_group__ - # is set when the exception group is created (and it is preserved on - # splits). So __traceback_group__ + __traceback__ - # gives us the full path. - import types - self.__traceback__ = None # will be set when the traceback group is raised - self.__traceback_group__ = TracebackGroup(self.excs) - - def split(self, E): - """Split an ExceptionGroup to extract exceptions matching E - - returns two new ExceptionGroups: match, rest of the exceptions of - self that match E and those that don't. - match and rest have the same nested structure as self. - E can be a type or tuple of types. - """ - match, rest = [], [] - for e in self.excs: - if isinstance(e, ExceptionGroup): # recurse - e_match, e_rest = e.split(E) - match.append(e_match) - rest.append(e_rest) - else: - if isinstance(e, E): - match.append(e) - e_match, e_rest = e, None - else: - rest.append(e) - return ExceptionGroup(match),ExceptionGroup(rest) - - def push_frame(self, frame): - import types - self.__traceback__ = types.TracebackType( - self.__traceback__, frame, 0, 0) - - @staticmethod - def render(exc, tb=None, indent=0): - print(exc) - tb = tb or exc.__traceback__ - while tb and not isinstance(tb, TracebackGroup): - print(' '*indent, tb.tb_frame) - tb = tb.tb_next - if isinstance(exc, ExceptionGroup): - tbg = exc.__traceback_group__ - assert isinstance(tbg, TracebackGroup) - indent += 4 - for e, t in tbg.tb_next_map.items(): - print('---------------------------------------') - ExceptionGroup.render(e, t, indent) - - def __iter__(self): - ''' iterate over the individual exceptions (flattens the tree) ''' - for e in self.excs: - if isinstance(e, ExceptionGroup): - for e_ in e: - yield e_ - else: - yield e - - def __repr__(self): - return f"ExceptionGroup({self.excs})" - __all__ = [n for n in globals() if n[:1] != '_'] diff --git a/exception_group.py b/exception_group.py deleted file mode 100644 index c6fb0187632430..00000000000000 --- a/exception_group.py +++ /dev/null @@ -1,79 +0,0 @@ -import sys -import traceback -import types - - -def f(i=0): raise ValueError(f'bad value: f{i}') -def f1(): f(1) - -def g(i=0): raise ValueError(f'bad value: g{i}') -def g1(): g(1) - -def h(i=0): raise TypeError(f'bad type: h{i}') -def h1(): h(1) - -def aggregator(): - excs = set() - for c in (f, g): - try: - c() - except Exception as e: - excs.add(e) - raise types.ExceptionGroup(excs) - -def aggregator1(): - excs = set() - for c in (f1, g1, aggregator): - try: - c() - except (Exception, types.ExceptionGroup) as e: - excs.add(e) - eg = types.ExceptionGroup(excs) - raise eg - -def propagator(): - aggregator1() - -def get_exception_group(): - try: - propagator() - except types.ExceptionGroup as e: - return e - -def handle_type_errors(): - try: - propagator() - except types.ExceptionGroup as e: - TEs, rest = e.split(TypeError) - return TEs, rest - -def handle_value_errors(): - try: - propagator() - except types.ExceptionGroup as e: - VEs, rest = e.split(ValueError) - return VEs, rest - - -def main(): - print (">>>>>>>>>>>>>>>>>> get_exception_group <<<<<<<<<<<<<<<<<<<<") - e = get_exception_group() - types.ExceptionGroup.render(e) - - print (">>>>>>>>>>>>>>>>>> handle_type_errors <<<<<<<<<<<<<<<<<<<<") - - TEs, rest = handle_type_errors() - print ("\n\n\n ------------- The split-off Type Errors:") - types.ExceptionGroup.render(TEs) - print ("\n\n\n ------------- The remaining unhandled:") - types.ExceptionGroup.render(rest) - - print (">>>>>>>>>>>>>>>>>> handle_value_errors <<<<<<<<<<<<<<<<<<<<") - VEs, rest = handle_value_errors() - print ("\n\n\n ------------- The split-off Value Errors:") - types.ExceptionGroup.render(VEs) - print ("\n\n\n ------------- The remaining unhandled:") - types.ExceptionGroup.render(rest) - -if __name__ == '__main__': - main() diff --git a/exception_group_test.py b/exception_group_test.py new file mode 100644 index 00000000000000..fde7c1fe90cc19 --- /dev/null +++ b/exception_group_test.py @@ -0,0 +1,79 @@ +import sys +import traceback +import exception_group + + +def f(i=0): raise ValueError(f'bad value: f{i}') +def f1(): f(1) + +def g(i=0): raise ValueError(f'bad value: g{i}') +def g1(): g(1) + +def h(i=0): raise TypeError(f'bad type: h{i}') +def h1(): h(1) + +def aggregator(): + excs = set() + for c in (f, g, h): + try: + c() + except Exception as e: + excs.add(e) + raise exception_group.ExceptionGroup(excs) + +def aggregator1(): + excs = set() + for c in (f1, g1, h1, aggregator): + try: + c() + except (Exception, exception_group.ExceptionGroup) as e: + excs.add(e) + eg = exception_group.ExceptionGroup(excs) + raise eg + +def propagator(): + aggregator1() + +def get_exception_group(): + try: + propagator() + except exception_group.ExceptionGroup as e: + return e + +def handle_type_errors(): + try: + propagator() + except exception_group.ExceptionGroup as e: + TEs, rest = e.split(TypeError) + return TEs, rest + +def handle_value_errors(): + try: + propagator() + except exception_group.ExceptionGroup as e: + VEs, rest = e.split(ValueError) + return VEs, rest + + +def main(): + print ("\n\n>>>>>>>>>>>>>>>>>> get_exception_group <<<<<<<<<<<<<<<<<<<<") + e = get_exception_group() + exception_group.ExceptionGroup.render(e) + + print ("\n\n>>>>>>>>>>>>>>>>>> handle_type_errors <<<<<<<<<<<<<<<<<<<<") + + TEs, rest = handle_type_errors() + print ("\n ------------- The split-off Type Errors:") + exception_group.ExceptionGroup.render(TEs) + print ("\n\n\n ------------- The remaining unhandled:") + exception_group.ExceptionGroup.render(rest) + + print ("\n\n>>>>>>>>>>>>>>>>>> handle_value_errors <<<<<<<<<<<<<<<<<<<<") + VEs, rest = handle_value_errors() + print ("\n ------------- The split-off Value Errors:") + exception_group.ExceptionGroup.render(VEs) + print ("\n ------------- The remaining unhandled:") + exception_group.ExceptionGroup.render(rest) + +if __name__ == '__main__': + main() diff --git a/output.txt b/output.txt index d49caec3559f47..7948c686719820 100644 --- a/output.txt +++ b/output.txt @@ -1,76 +1,100 @@ Running Release|Win32 interpreter... + + >>>>>>>>>>>>>>>>>> get_exception_group <<<<<<<<<<<<<<<<<<<< -{ExceptionGroup({ValueError('bad value: f0'), ValueError('bad value: g0')}), ValueError('bad value: g1'), ValueError('bad value: f1')} - - - +{TypeError('bad type: h1'), ValueError('bad value: f1'), ValueError('bad value: g1'), ExceptionGroup({ValueError('bad value: g0'), TypeError('bad type: h0'), ValueError('bad value: f0')})} + + + --------------------------------------- -bad value: f0 - - +bad type: h1 + + --------------------------------------- -bad value: g0 - - +bad value: f1 + + --------------------------------------- bad value: g1 - - + + --------------------------------------- -bad value: f1 - - ->>>>>>>>>>>>>>>>>> handle_type_errors <<<<<<<<<<<<<<<<<<<< +bad value: g0 + + +--------------------------------------- +bad type: h0 + + +--------------------------------------- +bad value: f0 + + +>>>>>>>>>>>>>>>>>> handle_type_errors <<<<<<<<<<<<<<<<<<<< ------------- The split-off Type Errors: -[ExceptionGroup(set())] +[ExceptionGroup({TypeError('bad type: h0')}), TypeError('bad type: h1')] +--------------------------------------- +bad type: h0 + + +--------------------------------------- +bad type: h1 + + ------------- The remaining unhandled: -[ExceptionGroup({ValueError('bad value: f0'), ValueError('bad value: g0')}), ValueError('bad value: f1'), ValueError('bad value: g1')] +[ValueError('bad value: g1'), ExceptionGroup({ValueError('bad value: g0'), ValueError('bad value: f0')}), ValueError('bad value: f1')] --------------------------------------- bad value: g1 - - + + --------------------------------------- -bad value: f0 - - +bad value: f1 + + --------------------------------------- bad value: g0 - - + + --------------------------------------- -bad value: f1 - - ->>>>>>>>>>>>>>>>>> handle_value_errors <<<<<<<<<<<<<<<<<<<< +bad value: f0 + + +>>>>>>>>>>>>>>>>>> handle_value_errors <<<<<<<<<<<<<<<<<<<< ------------- The split-off Value Errors: -[ValueError('bad value: f1'), ExceptionGroup({ValueError('bad value: g0'), ValueError('bad value: f0')}), ValueError('bad value: g1')] +[ValueError('bad value: g1'), ExceptionGroup({ValueError('bad value: g0'), ValueError('bad value: f0')}), ValueError('bad value: f1')] --------------------------------------- bad value: g0 - - + + --------------------------------------- bad value: f0 - - ---------------------------------------- -bad value: f1 - - + + --------------------------------------- bad value: g1 - - - - + + +--------------------------------------- +bad value: f1 + + ------------- The remaining unhandled: -[ExceptionGroup(set())] +[TypeError('bad type: h1'), ExceptionGroup({TypeError('bad type: h0')})] +--------------------------------------- +bad type: h1 + + +--------------------------------------- +bad type: h0 + + From 3b4254cb2eabe42c8d6d0320e5e75cf2abbde43c Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Wed, 28 Oct 2020 17:52:14 +0000 Subject: [PATCH 11/73] added Lib/exception_group.py --- Lib/exception_group.py | 83 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 Lib/exception_group.py diff --git a/Lib/exception_group.py b/Lib/exception_group.py new file mode 100644 index 00000000000000..d68a285d26244f --- /dev/null +++ b/Lib/exception_group.py @@ -0,0 +1,83 @@ + + + +class TracebackGroup: + def __init__(self, excs): + # TODO: Oy, this needs to be a weak key dict, but exceptions + # are not weakreffable. + self.tb_next_map = {} + self._init_map(excs) + + def _init_map(self, excs): + for e in excs: + if isinstance(e, ExceptionGroup): + for e_ in e.excs: + self.tb_next_map[e_] = e_.__traceback__ + else: + self.tb_next_map[e] = e.__traceback__.tb_next + +class ExceptionGroup(BaseException): + + def __init__(self, excs, tb=None): + self.excs = set(excs) + # self.__traceback__ is updated as usual, but self.__traceback_group__ + # is set when the exception group is created (and it is preserved on + # splits). So __traceback_group__ + __traceback__ + # gives us the full path. + self.__traceback__ = tb + self.__traceback_group__ = TracebackGroup(self.excs) + + def split(self, E): + """Split an ExceptionGroup to extract exceptions matching E + + returns two new ExceptionGroups: match, rest of the exceptions of + self that match E and those that don't. + match and rest have the same nested structure as self. + E can be a type or tuple of types. + """ + match, rest = [], [] + for e in self.excs: + if isinstance(e, ExceptionGroup): # recurse + e_match, e_rest = e.split(E) + match.append(e_match) + rest.append(e_rest) + else: + if isinstance(e, E): + match.append(e) + e_match, e_rest = e, None + else: + rest.append(e) + return (ExceptionGroup(match, tb=self.__traceback__), + ExceptionGroup(rest, tb=self.__traceback__)) + + def push_frame(self, frame): + import types + self.__traceback__ = types.TracebackType( + self.__traceback__, frame, 0, 0) + + @staticmethod + def render(exc, tb=None, indent=0): + print(exc) + tb = tb or exc.__traceback__ + while tb and not isinstance(tb, TracebackGroup): + print(' '*indent, tb.tb_frame) + tb = tb.tb_next + if isinstance(exc, ExceptionGroup): + tbg = exc.__traceback_group__ + assert isinstance(tbg, TracebackGroup) + indent += 4 + for e, t in tbg.tb_next_map.items(): + print('---------------------------------------') + ExceptionGroup.render(e, t, indent) + + def __iter__(self): + ''' iterate over the individual exceptions (flattens the tree) ''' + for e in self.excs: + if isinstance(e, ExceptionGroup): + for e_ in e: + yield e_ + else: + yield e + + def __repr__(self): + return f"ExceptionGroup({self.excs})" \ No newline at end of file From 79995cde56d4a52f9420848ad16e40f312dbb863 Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Wed, 28 Oct 2020 17:54:16 +0000 Subject: [PATCH 12/73] refreshed output --- Python/traceback.c | 1 - output.txt | 122 +++++++++++++++++++++++++-------------------- 2 files changed, 67 insertions(+), 56 deletions(-) diff --git a/Python/traceback.c b/Python/traceback.c index 11eb2f268256a9..708678facf7c31 100644 --- a/Python/traceback.c +++ b/Python/traceback.c @@ -141,7 +141,6 @@ tb_next_set(PyTracebackObject *self, PyObject *new_next, void *Py_UNUSED(_)) } - static PyMethodDef tb_methods[] = { {"__dir__", (PyCFunction)tb_dir, METH_NOARGS}, {NULL, NULL, 0, NULL}, diff --git a/output.txt b/output.txt index 7948c686719820..ddb1271bd6cdb2 100644 --- a/output.txt +++ b/output.txt @@ -2,99 +2,111 @@ Running Release|Win32 interpreter... >>>>>>>>>>>>>>>>>> get_exception_group <<<<<<<<<<<<<<<<<<<< -{TypeError('bad type: h1'), ValueError('bad value: f1'), ValueError('bad value: g1'), ExceptionGroup({ValueError('bad value: g0'), TypeError('bad type: h0'), ValueError('bad value: f0')})} - - - +{ValueError('bad value: g1'), TypeError('bad type: h1'), ValueError('bad value: f1'), ExceptionGroup({ValueError('bad value: f0'), ValueError('bad value: g0'), TypeError('bad type: h0')})} + + + +--------------------------------------- +bad value: g1 + + --------------------------------------- bad type: h1 - - + + --------------------------------------- bad value: f1 - - + + --------------------------------------- -bad value: g1 - - +bad value: f0 + + --------------------------------------- bad value: g0 - - + + --------------------------------------- bad type: h0 - - ---------------------------------------- -bad value: f0 - - + + >>>>>>>>>>>>>>>>>> handle_type_errors <<<<<<<<<<<<<<<<<<<< ------------- The split-off Type Errors: -[ExceptionGroup({TypeError('bad type: h0')}), TypeError('bad type: h1')] ---------------------------------------- -bad type: h0 - - +[TypeError('bad type: h1'), ExceptionGroup({TypeError('bad type: h0')})] + + + --------------------------------------- bad type: h1 - - + + +--------------------------------------- +bad type: h0 + + ------------- The remaining unhandled: [ValueError('bad value: g1'), ExceptionGroup({ValueError('bad value: g0'), ValueError('bad value: f0')}), ValueError('bad value: f1')] + + + --------------------------------------- bad value: g1 - - ---------------------------------------- -bad value: f1 - - + + --------------------------------------- bad value: g0 - - + + --------------------------------------- bad value: f0 - - + + +--------------------------------------- +bad value: f1 + + >>>>>>>>>>>>>>>>>> handle_value_errors <<<<<<<<<<<<<<<<<<<< ------------- The split-off Value Errors: -[ValueError('bad value: g1'), ExceptionGroup({ValueError('bad value: g0'), ValueError('bad value: f0')}), ValueError('bad value: f1')] ---------------------------------------- -bad value: g0 - - ---------------------------------------- -bad value: f0 - - +[ValueError('bad value: g1'), ValueError('bad value: f1'), ExceptionGroup({ValueError('bad value: g0'), ValueError('bad value: f0')})] + + + --------------------------------------- bad value: g1 - - + + --------------------------------------- bad value: f1 - - + + +--------------------------------------- +bad value: g0 + + +--------------------------------------- +bad value: f0 + + ------------- The remaining unhandled: [TypeError('bad type: h1'), ExceptionGroup({TypeError('bad type: h0')})] ---------------------------------------- -bad type: h1 - - + + + --------------------------------------- bad type: h0 - - + + +--------------------------------------- +bad type: h1 + + From 90700b7abad436b7f75dd8e4c89b3951642b34ad Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Wed, 28 Oct 2020 23:41:27 +0000 Subject: [PATCH 13/73] added unit tests checking the structure of a simple and nested EG --- Lib/test/test_exception_group.py | 163 +++++++++++++++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 Lib/test/test_exception_group.py diff --git a/Lib/test/test_exception_group.py b/Lib/test/test_exception_group.py new file mode 100644 index 00000000000000..3bf1a46ad8739e --- /dev/null +++ b/Lib/test/test_exception_group.py @@ -0,0 +1,163 @@ + +import unittest +from exception_group import ExceptionGroup, TracebackGroup + + +class ExceptionGroupTestBase(unittest.TestCase): + def assertExceptionsMatch(self, excs1, excs2): + """ Assert that two exception lists match in type and arg """ + self.assertEqual(len(excs1), len(excs2)) + self.assertSequenceEqual(sorted(str(type(e)) for e in excs1), + sorted(str(type(e)) for e in excs2)) + self.assertSequenceEqual(sorted(str(e.args[0]) for e in excs1), + sorted(str(e.args[0]) for e in excs2)) + + +class ExceptionGroupTestUtils(ExceptionGroupTestBase): + def raiseValueError(self, v): + raise ValueError(str(v)) + + def raiseTypeError(self, t): + raise TypeError(t) + + def get_test_exceptions(self, x): + return [ + (self.raiseValueError, ValueError, x+1), + (self.raiseTypeError, TypeError, 'int'), + (self.raiseValueError, ValueError, x+2), + (self.raiseValueError, ValueError, x+3), + (self.raiseTypeError, TypeError, 'list'), + ] + + def get_test_exceptions_list(self, x): + return [t(arg) for _, t, arg in self.get_test_exceptions(x)] + + def simple_exception_group(self, x): + excs = [] + for f, _, arg in self.get_test_exceptions(x): + try: + f(arg) + except Exception as e: + excs.append(e) + raise ExceptionGroup(excs) + + def nested_exception_group(self): + excs = [] + for x in [1,2,3]: + try: + self.simple_exception_group(x) + except ExceptionGroup as e: + excs.append(e) + raise ExceptionGroup(excs) + + def funcnames(self, tb): + """ Extract function names from a traceback """ + funcname = lambda tb_frame: tb_frame.f_code.co_name + names = [] + while tb: + names.append(funcname(tb.tb_frame)) + tb = tb.tb_next + return names + + def test_utility_functions(self): + self.assertRaises(ValueError, self.raiseValueError, 42) + self.assertRaises(TypeError, self.raiseTypeError, float) + self.assertRaises(ExceptionGroup, self.simple_exception_group, 42) + self.assertRaises(ExceptionGroup, self.nested_exception_group) + + test_excs = self.get_test_exceptions_list(42) + self.assertEqual(len(test_excs), 5) + expected = [("TypeError", 'int'), + ("TypeError", 'list'), + ("ValueError", 43), + ("ValueError", 44), + ("ValueError", 45)] + self.assertSequenceEqual(expected, + sorted((type(e).__name__, e.args[0]) for e in test_excs)) + +class ExceptionGroupConstructionTests(ExceptionGroupTestUtils): + + def test_construction_simple(self): + # create a simple exception group and check that + # it is constructed as expected + try: + self.simple_exception_group(0) + self.assertFalse(True, 'exception not raised') + except ExceptionGroup as eg: + # check eg.excs + self.assertIsInstance(eg.excs, set) + self.assertExceptionsMatch(eg.excs, self.get_test_exceptions_list(0)) + + # check iteration + self.assertEqual(list(eg), list(eg.excs)) + + # check eg.__traceback__ + self.assertEqual(self.funcnames(eg.__traceback__), + ['test_construction_simple', 'simple_exception_group']) + + # check eg.__traceback_group__ + tbg = eg.__traceback_group__ + self.assertEqual(len(tbg.tb_next_map), 5) + self.assertEqual(tbg.tb_next_map.keys(), eg.excs) + for e, tb in tbg.tb_next_map.items(): + self.assertEqual(self.funcnames(tb), ['raise'+type(e).__name__]) + + else: + self.assertFalse(True, 'exception not caught') + + def test_construction_nested(self): + # create a nested exception group and check that + # it is constructed as expected + try: + self.nested_exception_group() + self.assertFalse(True, 'exception not raised') + except ExceptionGroup as eg: + # check eg.excs + self.assertIsInstance(eg.excs, set) + self.assertEqual(len(eg.excs), 3) + + # each of eg.excs is an EG with 3xValueError and 2xTypeErrors + all_excs = [] + for e in eg.excs: + self.assertIsInstance(e, ExceptionGroup) + self.assertEqual(len(e.excs), 5) + etypes = [type(e) for e in e.excs] + self.assertEqual(etypes.count(ValueError), 3) + self.assertEqual(etypes.count(TypeError), 2) + all_excs.extend(e.excs) + + expected_excs = [] + expected_excs.extend(self.get_test_exceptions_list(1)) + expected_excs.extend(self.get_test_exceptions_list(2)) + expected_excs.extend(self.get_test_exceptions_list(3)) + self.assertExceptionsMatch(all_excs, expected_excs) + + # check iteration + self.assertEqual(list(eg), all_excs) + + # check eg.__traceback__ + tb = eg.__traceback__ + self.assertEqual(self.funcnames(tb), + ['test_construction_nested', 'nested_exception_group']) + + # check eg.__traceback_group__ + tbg = eg.__traceback_group__ + self.assertEqual(len(tbg.tb_next_map), 15) + + self.assertEqual(list(tbg.tb_next_map.keys()), all_excs) + for e, tb in tbg.tb_next_map.items(): + self.assertEqual(self.funcnames(tb), + ['simple_exception_group', 'raise'+type(e).__name__]) + + else: + self.assertFalse(True, 'exception not caught') + +class ExceptionGroupSplitTests(ExceptionGroupTestUtils): + pass # TODO + +class ExceptionGroupCatchTests(ExceptionGroupTestUtils): + pass # TODO - write the catch context manager and add tests + + +if __name__ == '__main__': + unittest.main() From 3257e5b656bea06992a54f7086ab0f3438b45725 Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Thu, 29 Oct 2020 10:58:55 +0000 Subject: [PATCH 14/73] assertExceptionMatchesTemplate instad of assertExceptionsMatch to compare the whole structure. Added stubs for split tests --- Lib/exception_group.py | 7 ++- Lib/test/test_exception_group.py | 75 ++++++++++++++++++++++++-------- 2 files changed, 60 insertions(+), 22 deletions(-) diff --git a/Lib/exception_group.py b/Lib/exception_group.py index d68a285d26244f..a272ba43a446e1 100644 --- a/Lib/exception_group.py +++ b/Lib/exception_group.py @@ -19,11 +19,10 @@ def _init_map(self, excs): class ExceptionGroup(BaseException): def __init__(self, excs, tb=None): - self.excs = set(excs) + self.excs = excs # self.__traceback__ is updated as usual, but self.__traceback_group__ - # is set when the exception group is created (and it is preserved on - # splits). So __traceback_group__ + __traceback__ - # gives us the full path. + # is set when the exception group is created. + # __traceback_group__ and __traceback__ combine to give the full path. self.__traceback__ = tb self.__traceback_group__ = TracebackGroup(self.excs) diff --git a/Lib/test/test_exception_group.py b/Lib/test/test_exception_group.py index 3bf1a46ad8739e..fd36ac284edd1d 100644 --- a/Lib/test/test_exception_group.py +++ b/Lib/test/test_exception_group.py @@ -1,21 +1,26 @@ import unittest +import collections.abc from exception_group import ExceptionGroup, TracebackGroup class ExceptionGroupTestBase(unittest.TestCase): - def assertExceptionsMatch(self, excs1, excs2): - """ Assert that two exception lists match in type and arg """ - self.assertEqual(len(excs1), len(excs2)) - self.assertSequenceEqual(sorted(str(type(e)) for e in excs1), - sorted(str(type(e)) for e in excs2)) - self.assertSequenceEqual(sorted(str(e.args[0]) for e in excs1), - sorted(str(e.args[0]) for e in excs2)) + def assertExceptionMatchesTemplate(self, exc, template): + """ Assert that the exception matches the template """ + if isinstance(exc, ExceptionGroup): + self.assertIsInstance(template, collections.abc.Sequence) + self.assertEqual(len(exc.excs), len(template)) + for e, t in zip(exc.excs, template): + self.assertExceptionMatchesTemplate(e, t) + else: + self.assertIsInstance(template, BaseException) + self.assertEqual(type(exc), type(template)) + self.assertEqual(exc.args, template.args) class ExceptionGroupTestUtils(ExceptionGroupTestBase): def raiseValueError(self, v): - raise ValueError(str(v)) + raise ValueError(v) def raiseTypeError(self, t): raise TypeError(t) @@ -85,8 +90,8 @@ def test_construction_simple(self): self.assertFalse(True, 'exception not raised') except ExceptionGroup as eg: # check eg.excs - self.assertIsInstance(eg.excs, set) - self.assertExceptionsMatch(eg.excs, self.get_test_exceptions_list(0)) + self.assertIsInstance(eg.excs, collections.abc.Sequence) + self.assertExceptionMatchesTemplate(eg, self.get_test_exceptions_list(0)) # check iteration self.assertEqual(list(eg), list(eg.excs)) @@ -98,7 +103,7 @@ def test_construction_simple(self): # check eg.__traceback_group__ tbg = eg.__traceback_group__ self.assertEqual(len(tbg.tb_next_map), 5) - self.assertEqual(tbg.tb_next_map.keys(), eg.excs) + self.assertEqual(tbg.tb_next_map.keys(), set(eg.excs)) for e, tb in tbg.tb_next_map.items(): self.assertEqual(self.funcnames(tb), ['raise'+type(e).__name__]) @@ -113,7 +118,7 @@ def test_construction_nested(self): self.assertFalse(True, 'exception not raised') except ExceptionGroup as eg: # check eg.excs - self.assertIsInstance(eg.excs, set) + self.assertIsInstance(eg.excs, collections.abc.Sequence) self.assertEqual(len(eg.excs), 3) # each of eg.excs is an EG with 3xValueError and 2xTypeErrors @@ -127,10 +132,10 @@ def test_construction_nested(self): all_excs.extend(e.excs) expected_excs = [] - expected_excs.extend(self.get_test_exceptions_list(1)) - expected_excs.extend(self.get_test_exceptions_list(2)) - expected_excs.extend(self.get_test_exceptions_list(3)) - self.assertExceptionsMatch(all_excs, expected_excs) + expected_excs.append(self.get_test_exceptions_list(1)) + expected_excs.append(self.get_test_exceptions_list(2)) + expected_excs.append(self.get_test_exceptions_list(3)) + self.assertExceptionMatchesTemplate(eg, expected_excs) # check iteration self.assertEqual(list(eg), all_excs) @@ -148,12 +153,46 @@ def test_construction_nested(self): for e, tb in tbg.tb_next_map.items(): self.assertEqual(self.funcnames(tb), ['simple_exception_group', 'raise'+type(e).__name__]) - else: self.assertFalse(True, 'exception not caught') class ExceptionGroupSplitTests(ExceptionGroupTestUtils): - pass # TODO + def test_split_simple(self): + try: + self.simple_exception_group(5) + self.assertFalse(True, 'exception not raised') + except ExceptionGroup as eg: + syntaxError, ref = eg.split(SyntaxError) + # TODO: check everything + valueError, ref = eg.split(ValueError) + # TODO: check everything + typeError, ref = eg.split(TypeError) + # TODO: check everything + valueError, ref = eg.split((ValueError, SyntaxError)) + # TODO: check everything + valueError, ref = eg.split((ValueError, TypeError)) + # TODO: check everything + else: + self.assertFalse(True, 'exception not caught') + + def test_split_nested(self): + try: + self.nested_exception_group() + self.assertFalse(True, 'exception not raised') + except ExceptionGroup as eg: + syntaxError, ref = eg.split(SyntaxError) + # TODO: check everything + valueError, ref = eg.split(ValueError) + # TODO: check everything + typeError, ref = eg.split(TypeError) + # TODO: check everything + valueError, ref = eg.split((ValueError, SyntaxError)) + # TODO: check everything + valueError, ref = eg.split((ValueError, TypeError)) + # TODO: check everything + else: + self.assertFalse(True, 'exception not caught') + class ExceptionGroupCatchTests(ExceptionGroupTestUtils): pass # TODO - write the catch context manager and add tests From e95fd95b623bb01fa288cc16f9ac33bf6ffdc595 Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Thu, 29 Oct 2020 12:49:15 +0000 Subject: [PATCH 15/73] added simple split tests --- Lib/exception_group.py | 3 - Lib/test/test_exception_group.py | 108 ++++++++++++++++++++++++------- 2 files changed, 84 insertions(+), 27 deletions(-) diff --git a/Lib/exception_group.py b/Lib/exception_group.py index a272ba43a446e1..dea56354bdf2cc 100644 --- a/Lib/exception_group.py +++ b/Lib/exception_group.py @@ -6,9 +6,6 @@ def __init__(self, excs): # TODO: Oy, this needs to be a weak key dict, but exceptions # are not weakreffable. self.tb_next_map = {} - self._init_map(excs) - - def _init_map(self, excs): for e in excs: if isinstance(e, ExceptionGroup): for e_ in e.excs: diff --git a/Lib/test/test_exception_group.py b/Lib/test/test_exception_group.py index fd36ac284edd1d..cda02cbdab05b3 100644 --- a/Lib/test/test_exception_group.py +++ b/Lib/test/test_exception_group.py @@ -104,7 +104,8 @@ def test_construction_simple(self): tbg = eg.__traceback_group__ self.assertEqual(len(tbg.tb_next_map), 5) self.assertEqual(tbg.tb_next_map.keys(), set(eg.excs)) - for e, tb in tbg.tb_next_map.items(): + for e in eg.excs: + tb = tbg.tb_next_map[e] self.assertEqual(self.funcnames(tb), ['raise'+type(e).__name__]) else: @@ -131,47 +132,103 @@ def test_construction_nested(self): self.assertEqual(etypes.count(TypeError), 2) all_excs.extend(e.excs) - expected_excs = [] - expected_excs.append(self.get_test_exceptions_list(1)) - expected_excs.append(self.get_test_exceptions_list(2)) - expected_excs.append(self.get_test_exceptions_list(3)) + expected_excs = [self.get_test_exceptions_list(i) for i in [1,2,3]] self.assertExceptionMatchesTemplate(eg, expected_excs) # check iteration self.assertEqual(list(eg), all_excs) # check eg.__traceback__ - tb = eg.__traceback__ - self.assertEqual(self.funcnames(tb), + self.assertEqual(self.funcnames(eg.__traceback__), ['test_construction_nested', 'nested_exception_group']) # check eg.__traceback_group__ tbg = eg.__traceback_group__ self.assertEqual(len(tbg.tb_next_map), 15) - self.assertEqual(list(tbg.tb_next_map.keys()), all_excs) - for e, tb in tbg.tb_next_map.items(): + for e in all_excs: + tb = tbg.tb_next_map[e] self.assertEqual(self.funcnames(tb), ['simple_exception_group', 'raise'+type(e).__name__]) else: self.assertFalse(True, 'exception not caught') class ExceptionGroupSplitTests(ExceptionGroupTestUtils): + def _check_traceback_group_after_split(self, source_eg, eg): + tb_next_map = eg.__traceback_group__.tb_next_map + source_tb_next_map = source_eg.__traceback_group__.tb_next_map + for e in eg: + self.assertEqual(self.funcnames(tb_next_map[e]), + self.funcnames(source_tb_next_map[e])) + self.assertEqual(len(tb_next_map), len(list(eg))) + + def _split_exception_group(self, eg, types): + """ Split an EG and do some sanity checks on the result """ + self.assertIsInstance(eg, ExceptionGroup) + fnames = self.funcnames(eg.__traceback__) + all_excs = list(eg) + + match, rest = eg.split(types) + + self.assertIsInstance(match, ExceptionGroup) + self.assertIsInstance(rest, ExceptionGroup) + + self.assertEqual(len(all_excs), len(list(eg))) + self.assertEqual(len(all_excs), len(list(match)) + len(list(rest))) + for e in all_excs: + self.assertIn(e, eg) + # every exception in all_excs is in eg and + # in exactly one of match and rest + self.assertNotEqual(e in match, e in rest) + + for e in match: + self.assertIsInstance(e, types) + for e in rest: + self.assertNotIsInstance(e, types) + + # traceback was copied over + self.assertEqual(self.funcnames(match.__traceback__), fnames) + self.assertEqual(self.funcnames(rest.__traceback__), fnames) + + self._check_traceback_group_after_split(eg, match) + self._check_traceback_group_after_split(eg, rest) + return match, rest + def test_split_simple(self): try: self.simple_exception_group(5) self.assertFalse(True, 'exception not raised') except ExceptionGroup as eg: - syntaxError, ref = eg.split(SyntaxError) - # TODO: check everything - valueError, ref = eg.split(ValueError) - # TODO: check everything - typeError, ref = eg.split(TypeError) - # TODO: check everything - valueError, ref = eg.split((ValueError, SyntaxError)) - # TODO: check everything - valueError, ref = eg.split((ValueError, TypeError)) - # TODO: check everything + fnames = ['test_split_simple', 'simple_exception_group'] + self.assertEqual(self.funcnames(eg.__traceback__), fnames) + + allExceptions = self.get_test_exceptions_list(5) + self.assertExceptionMatchesTemplate(eg, allExceptions) + + match, rest = self._split_exception_group(eg, SyntaxError) + self.assertExceptionMatchesTemplate(eg, allExceptions) + self.assertExceptionMatchesTemplate(match, []) + self.assertExceptionMatchesTemplate(rest, allExceptions) + + match, rest = self._split_exception_group(eg, ValueError) + self.assertExceptionMatchesTemplate(eg, allExceptions) + self.assertExceptionMatchesTemplate(match, [ValueError(i) for i in [6,7,8]]) + self.assertExceptionMatchesTemplate(rest, [TypeError(t) for t in ['int', 'list']]) + + match, rest = self._split_exception_group(eg, TypeError) + self.assertExceptionMatchesTemplate(eg, allExceptions) + self.assertExceptionMatchesTemplate(match, [TypeError(t) for t in ['int', 'list']]) + self.assertExceptionMatchesTemplate(rest, [ValueError(i) for i in [6,7,8]]) + + match, rest = self._split_exception_group(eg, (ValueError, SyntaxError)) + self.assertExceptionMatchesTemplate(eg, allExceptions) + self.assertExceptionMatchesTemplate(match, [ValueError(i) for i in [6,7,8]]) + self.assertExceptionMatchesTemplate(rest, [TypeError(t) for t in ['int', 'list']]) + + match, rest = self._split_exception_group(eg, (ValueError, TypeError)) + self.assertExceptionMatchesTemplate(eg, allExceptions) + self.assertExceptionMatchesTemplate(match, allExceptions) + self.assertExceptionMatchesTemplate(rest, []) else: self.assertFalse(True, 'exception not caught') @@ -180,15 +237,18 @@ def test_split_nested(self): self.nested_exception_group() self.assertFalse(True, 'exception not raised') except ExceptionGroup as eg: - syntaxError, ref = eg.split(SyntaxError) + self.assertEqual(self.funcnames(eg.__traceback__), + ['test_split_nested', 'nested_exception_group']) + + syntaxErrors, rest = eg.split(SyntaxError) # TODO: check everything - valueError, ref = eg.split(ValueError) + valueErrors, rest = eg.split(ValueError) # TODO: check everything - typeError, ref = eg.split(TypeError) + typeErrors, rest = eg.split(TypeError) # TODO: check everything - valueError, ref = eg.split((ValueError, SyntaxError)) + valueErrors, rest = eg.split((ValueError, SyntaxError)) # TODO: check everything - valueError, ref = eg.split((ValueError, TypeError)) + valueErrors, rest = eg.split((ValueError, TypeError)) # TODO: check everything else: self.assertFalse(True, 'exception not caught') From f729a569efaa04f8f7e86d76ee6fd83654f3f5b7 Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Thu, 29 Oct 2020 13:34:17 +0000 Subject: [PATCH 16/73] added split tests for nexted exception groups --- Lib/test/test_exception_group.py | 120 +++++++++++++++++++++---------- 1 file changed, 84 insertions(+), 36 deletions(-) diff --git a/Lib/test/test_exception_group.py b/Lib/test/test_exception_group.py index cda02cbdab05b3..2408677df2ad2b 100644 --- a/Lib/test/test_exception_group.py +++ b/Lib/test/test_exception_group.py @@ -6,13 +6,13 @@ class ExceptionGroupTestBase(unittest.TestCase): - def assertExceptionMatchesTemplate(self, exc, template): + def assertMatchesTemplate(self, exc, template): """ Assert that the exception matches the template """ if isinstance(exc, ExceptionGroup): self.assertIsInstance(template, collections.abc.Sequence) self.assertEqual(len(exc.excs), len(template)) for e, t in zip(exc.excs, template): - self.assertExceptionMatchesTemplate(e, t) + self.assertMatchesTemplate(e, t) else: self.assertIsInstance(template, BaseException) self.assertEqual(type(exc), type(template)) @@ -64,7 +64,22 @@ def funcnames(self, tb): tb = tb.tb_next return names - def test_utility_functions(self): + def _reduce(self, template, types): + """ reduce a nested list of types to certain types + + The result is a nested list of the same shape as template, + but with only exceptions that match types + """ + if isinstance(template, collections.abc.Sequence): + res = [self._reduce(t, types) for t in template] + return [x for x in res if x is not None] + elif isinstance(template, types): + return template + else: + return None + +class ExceptionGroupTestUtilsTests(ExceptionGroupTestUtils): + def test_basic_utility_functions(self): self.assertRaises(ValueError, self.raiseValueError, 42) self.assertRaises(TypeError, self.raiseTypeError, float) self.assertRaises(ExceptionGroup, self.simple_exception_group, 42) @@ -80,6 +95,22 @@ def test_utility_functions(self): self.assertSequenceEqual(expected, sorted((type(e).__name__, e.args[0]) for e in test_excs)) + def test_reduce(self): + te = TypeError('int') + se = SyntaxError('blah') + ve1 = ValueError(1) + ve2 = ValueError(2) + template = [[te, ve1], se, [ve2]] + reduce = self._reduce + self.assertEqual(reduce(template, ()), [[],[]]) + self.assertEqual(reduce(template, TypeError), [[te],[]]) + self.assertEqual(reduce(template, ValueError), [[ve1],[ve2]]) + self.assertEqual(reduce(template, SyntaxError), [[], se, []]) + self.assertEqual( + reduce(template, (TypeError, ValueError)), [[te, ve1], [ve2]]) + self.assertEqual( + reduce(template, (TypeError, SyntaxError)), [[te], se, []]) + class ExceptionGroupConstructionTests(ExceptionGroupTestUtils): def test_construction_simple(self): @@ -91,7 +122,7 @@ def test_construction_simple(self): except ExceptionGroup as eg: # check eg.excs self.assertIsInstance(eg.excs, collections.abc.Sequence) - self.assertExceptionMatchesTemplate(eg, self.get_test_exceptions_list(0)) + self.assertMatchesTemplate(eg, self.get_test_exceptions_list(0)) # check iteration self.assertEqual(list(eg), list(eg.excs)) @@ -132,8 +163,8 @@ def test_construction_nested(self): self.assertEqual(etypes.count(TypeError), 2) all_excs.extend(e.excs) - expected_excs = [self.get_test_exceptions_list(i) for i in [1,2,3]] - self.assertExceptionMatchesTemplate(eg, expected_excs) + eg_template = [self.get_test_exceptions_list(i) for i in [1,2,3]] + self.assertMatchesTemplate(eg, eg_template) # check iteration self.assertEqual(list(eg), all_excs) @@ -202,33 +233,33 @@ def test_split_simple(self): fnames = ['test_split_simple', 'simple_exception_group'] self.assertEqual(self.funcnames(eg.__traceback__), fnames) - allExceptions = self.get_test_exceptions_list(5) - self.assertExceptionMatchesTemplate(eg, allExceptions) + eg_template = self.get_test_exceptions_list(5) + self.assertMatchesTemplate(eg, eg_template) match, rest = self._split_exception_group(eg, SyntaxError) - self.assertExceptionMatchesTemplate(eg, allExceptions) - self.assertExceptionMatchesTemplate(match, []) - self.assertExceptionMatchesTemplate(rest, allExceptions) + self.assertMatchesTemplate(eg, eg_template) + self.assertMatchesTemplate(match, []) + self.assertMatchesTemplate(rest, eg_template) match, rest = self._split_exception_group(eg, ValueError) - self.assertExceptionMatchesTemplate(eg, allExceptions) - self.assertExceptionMatchesTemplate(match, [ValueError(i) for i in [6,7,8]]) - self.assertExceptionMatchesTemplate(rest, [TypeError(t) for t in ['int', 'list']]) + self.assertMatchesTemplate(eg, eg_template) + self.assertMatchesTemplate(match, [ValueError(i) for i in [6,7,8]]) + self.assertMatchesTemplate(rest, [TypeError(t) for t in ['int', 'list']]) match, rest = self._split_exception_group(eg, TypeError) - self.assertExceptionMatchesTemplate(eg, allExceptions) - self.assertExceptionMatchesTemplate(match, [TypeError(t) for t in ['int', 'list']]) - self.assertExceptionMatchesTemplate(rest, [ValueError(i) for i in [6,7,8]]) + self.assertMatchesTemplate(eg, eg_template) + self.assertMatchesTemplate(match, [TypeError(t) for t in ['int', 'list']]) + self.assertMatchesTemplate(rest, [ValueError(i) for i in [6,7,8]]) match, rest = self._split_exception_group(eg, (ValueError, SyntaxError)) - self.assertExceptionMatchesTemplate(eg, allExceptions) - self.assertExceptionMatchesTemplate(match, [ValueError(i) for i in [6,7,8]]) - self.assertExceptionMatchesTemplate(rest, [TypeError(t) for t in ['int', 'list']]) + self.assertMatchesTemplate(eg, eg_template) + self.assertMatchesTemplate(match, [ValueError(i) for i in [6,7,8]]) + self.assertMatchesTemplate(rest, [TypeError(t) for t in ['int', 'list']]) match, rest = self._split_exception_group(eg, (ValueError, TypeError)) - self.assertExceptionMatchesTemplate(eg, allExceptions) - self.assertExceptionMatchesTemplate(match, allExceptions) - self.assertExceptionMatchesTemplate(rest, []) + self.assertMatchesTemplate(eg, eg_template) + self.assertMatchesTemplate(match, eg_template) + self.assertMatchesTemplate(rest, []) else: self.assertFalse(True, 'exception not caught') @@ -237,19 +268,36 @@ def test_split_nested(self): self.nested_exception_group() self.assertFalse(True, 'exception not raised') except ExceptionGroup as eg: - self.assertEqual(self.funcnames(eg.__traceback__), - ['test_split_nested', 'nested_exception_group']) - - syntaxErrors, rest = eg.split(SyntaxError) - # TODO: check everything - valueErrors, rest = eg.split(ValueError) - # TODO: check everything - typeErrors, rest = eg.split(TypeError) - # TODO: check everything - valueErrors, rest = eg.split((ValueError, SyntaxError)) - # TODO: check everything - valueErrors, rest = eg.split((ValueError, TypeError)) - # TODO: check everything + fnames = ['test_split_nested', 'nested_exception_group'] + self.assertEqual(self.funcnames(eg.__traceback__), fnames) + + eg_template = [self.get_test_exceptions_list(i) for i in [1,2,3]] + self.assertMatchesTemplate(eg, eg_template) + + match, rest = self._split_exception_group(eg, SyntaxError) + self.assertMatchesTemplate(eg, eg_template) + self.assertMatchesTemplate(match, [[],[],[]]) + self.assertMatchesTemplate(rest, eg_template) + + match, rest = self._split_exception_group(eg, ValueError) + self.assertMatchesTemplate(eg, eg_template) + self.assertMatchesTemplate(match, self._reduce(eg_template, ValueError)) + self.assertMatchesTemplate(rest, self._reduce(eg_template, TypeError)) + + match, rest = self._split_exception_group(eg, TypeError) + self.assertMatchesTemplate(eg, eg_template) + self.assertMatchesTemplate(match, self._reduce(eg_template, TypeError)) + self.assertMatchesTemplate(rest, self._reduce(eg_template, ValueError)) + + match, rest = self._split_exception_group(eg, (ValueError, SyntaxError)) + self.assertMatchesTemplate(eg, eg_template) + self.assertMatchesTemplate(match, self._reduce(eg_template, ValueError)) + self.assertMatchesTemplate(rest, self._reduce(eg_template, TypeError)) + + match, rest = self._split_exception_group(eg, (ValueError, TypeError)) + self.assertMatchesTemplate(eg, eg_template) + self.assertMatchesTemplate(match, eg_template) + self.assertMatchesTemplate(rest, [[],[],[]]) else: self.assertFalse(True, 'exception not caught') From f16f60110d6f613a2b5bd8adabbd1090f5f9d7aa Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Thu, 29 Oct 2020 13:37:40 +0000 Subject: [PATCH 17/73] deleted the old test script and output --- exception_group_test.py | 79 ---------------------------- output.txt | 112 ---------------------------------------- 2 files changed, 191 deletions(-) delete mode 100644 exception_group_test.py delete mode 100644 output.txt diff --git a/exception_group_test.py b/exception_group_test.py deleted file mode 100644 index fde7c1fe90cc19..00000000000000 --- a/exception_group_test.py +++ /dev/null @@ -1,79 +0,0 @@ -import sys -import traceback -import exception_group - - -def f(i=0): raise ValueError(f'bad value: f{i}') -def f1(): f(1) - -def g(i=0): raise ValueError(f'bad value: g{i}') -def g1(): g(1) - -def h(i=0): raise TypeError(f'bad type: h{i}') -def h1(): h(1) - -def aggregator(): - excs = set() - for c in (f, g, h): - try: - c() - except Exception as e: - excs.add(e) - raise exception_group.ExceptionGroup(excs) - -def aggregator1(): - excs = set() - for c in (f1, g1, h1, aggregator): - try: - c() - except (Exception, exception_group.ExceptionGroup) as e: - excs.add(e) - eg = exception_group.ExceptionGroup(excs) - raise eg - -def propagator(): - aggregator1() - -def get_exception_group(): - try: - propagator() - except exception_group.ExceptionGroup as e: - return e - -def handle_type_errors(): - try: - propagator() - except exception_group.ExceptionGroup as e: - TEs, rest = e.split(TypeError) - return TEs, rest - -def handle_value_errors(): - try: - propagator() - except exception_group.ExceptionGroup as e: - VEs, rest = e.split(ValueError) - return VEs, rest - - -def main(): - print ("\n\n>>>>>>>>>>>>>>>>>> get_exception_group <<<<<<<<<<<<<<<<<<<<") - e = get_exception_group() - exception_group.ExceptionGroup.render(e) - - print ("\n\n>>>>>>>>>>>>>>>>>> handle_type_errors <<<<<<<<<<<<<<<<<<<<") - - TEs, rest = handle_type_errors() - print ("\n ------------- The split-off Type Errors:") - exception_group.ExceptionGroup.render(TEs) - print ("\n\n\n ------------- The remaining unhandled:") - exception_group.ExceptionGroup.render(rest) - - print ("\n\n>>>>>>>>>>>>>>>>>> handle_value_errors <<<<<<<<<<<<<<<<<<<<") - VEs, rest = handle_value_errors() - print ("\n ------------- The split-off Value Errors:") - exception_group.ExceptionGroup.render(VEs) - print ("\n ------------- The remaining unhandled:") - exception_group.ExceptionGroup.render(rest) - -if __name__ == '__main__': - main() diff --git a/output.txt b/output.txt deleted file mode 100644 index ddb1271bd6cdb2..00000000000000 --- a/output.txt +++ /dev/null @@ -1,112 +0,0 @@ -Running Release|Win32 interpreter... - - ->>>>>>>>>>>>>>>>>> get_exception_group <<<<<<<<<<<<<<<<<<<< -{ValueError('bad value: g1'), TypeError('bad type: h1'), ValueError('bad value: f1'), ExceptionGroup({ValueError('bad value: f0'), ValueError('bad value: g0'), TypeError('bad type: h0')})} - - - ---------------------------------------- -bad value: g1 - - ---------------------------------------- -bad type: h1 - - ---------------------------------------- -bad value: f1 - - ---------------------------------------- -bad value: f0 - - ---------------------------------------- -bad value: g0 - - ---------------------------------------- -bad type: h0 - - - - ->>>>>>>>>>>>>>>>>> handle_type_errors <<<<<<<<<<<<<<<<<<<< - - ------------- The split-off Type Errors: -[TypeError('bad type: h1'), ExceptionGroup({TypeError('bad type: h0')})] - - - ---------------------------------------- -bad type: h1 - - ---------------------------------------- -bad type: h0 - - - - - - ------------- The remaining unhandled: -[ValueError('bad value: g1'), ExceptionGroup({ValueError('bad value: g0'), ValueError('bad value: f0')}), ValueError('bad value: f1')] - - - ---------------------------------------- -bad value: g1 - - ---------------------------------------- -bad value: g0 - - ---------------------------------------- -bad value: f0 - - ---------------------------------------- -bad value: f1 - - - - ->>>>>>>>>>>>>>>>>> handle_value_errors <<<<<<<<<<<<<<<<<<<< - - ------------- The split-off Value Errors: -[ValueError('bad value: g1'), ValueError('bad value: f1'), ExceptionGroup({ValueError('bad value: g0'), ValueError('bad value: f0')})] - - - ---------------------------------------- -bad value: g1 - - ---------------------------------------- -bad value: f1 - - ---------------------------------------- -bad value: g0 - - ---------------------------------------- -bad value: f0 - - - - ------------- The remaining unhandled: -[TypeError('bad type: h1'), ExceptionGroup({TypeError('bad type: h0')})] - - - ---------------------------------------- -bad type: h0 - - ---------------------------------------- -bad type: h1 - - From a4b31edc094af804b8c9c2315da26f4b8bb442c0 Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Thu, 29 Oct 2020 17:41:43 +0000 Subject: [PATCH 18/73] added ExceptionGroupCatcher and tests for simple (swallowing) handler --- Lib/exception_group.py | 73 ++++++++++++++++++++++++- Lib/test/test_exception_group.py | 94 ++++++++++++++++++++++++++++++-- 2 files changed, 161 insertions(+), 6 deletions(-) diff --git a/Lib/exception_group.py b/Lib/exception_group.py index dea56354bdf2cc..4480a52cd094b6 100644 --- a/Lib/exception_group.py +++ b/Lib/exception_group.py @@ -1,10 +1,12 @@ +import sys class TracebackGroup: def __init__(self, excs): # TODO: Oy, this needs to be a weak key dict, but exceptions # are not weakreffable. + # TODO: what if e is unhashable? self.tb_next_map = {} for e in excs: if isinstance(e, ExceptionGroup): @@ -75,5 +77,74 @@ def __iter__(self): else: yield e + def __len__(self): + l = 0 + for e in self.excs: + if isinstance(e, ExceptionGroup): + l += len(e) + else: + l += 1 + return l + def __repr__(self): - return f"ExceptionGroup({self.excs})" \ No newline at end of file + return f"ExceptionGroup({self.excs})" + + @staticmethod + def catch(types, handler): + return ExceptionGroupCatcher(types, handler) + +class ExceptionGroupCatcher: + """ Based on trio.MultiErrorCatcher """ + + def __init__(self, types, handler): + """ Context manager to catch and handle ExceptionGroups + + types: the exception types that this handler is interested in + handler: a function that takes an ExceptionGroup of the + matched type and does something with them + + Any unmatched exceptions are raised at the end as another + exception group + """ + self.types = types + self.handler = handler + + def __enter__(self): + pass + + def __exit__(self, etype, exc, tb): + if exc is not None and isinstance(exc, ExceptionGroup): + match, rest = exc.split(self.types) + + if not match: + # Let the interpreter reraise the exception + return False + + new_exception_group = self.handler(match) + if not new_exception_group and not rest: + # handled and swallowed all exceptions + return True + + if not new_exception_group: + to_raise = rest + elif not rest: + to_raise = new_exception_group + else: + # merge rest and new_exceptions + # keep the traceback from rest + to_raise = ExceptionGroup( + rest.excs + new_exception_group.excs, + tb = rest.__traceback__) + + # When we raise to_raise, Python will unconditionally blow + # away its __context__ attribute and replace it with the original + # exc we caught. So after we raise it, we have to pause it while + # it's in flight to put the correct __context__ back. + old_context = to_raise.__context__ + try: + raise to_raise + finally: + _, value, _ = sys.exc_info() + assert value is to_raise + value.__context__ = old_context + diff --git a/Lib/test/test_exception_group.py b/Lib/test/test_exception_group.py index 2408677df2ad2b..6dd6ede17a0d16 100644 --- a/Lib/test/test_exception_group.py +++ b/Lib/test/test_exception_group.py @@ -191,7 +191,7 @@ def _check_traceback_group_after_split(self, source_eg, eg): for e in eg: self.assertEqual(self.funcnames(tb_next_map[e]), self.funcnames(source_tb_next_map[e])) - self.assertEqual(len(tb_next_map), len(list(eg))) + self.assertEqual(len(tb_next_map), len(eg)) def _split_exception_group(self, eg, types): """ Split an EG and do some sanity checks on the result """ @@ -204,8 +204,8 @@ def _split_exception_group(self, eg, types): self.assertIsInstance(match, ExceptionGroup) self.assertIsInstance(rest, ExceptionGroup) - self.assertEqual(len(all_excs), len(list(eg))) - self.assertEqual(len(all_excs), len(list(match)) + len(list(rest))) + self.assertEqual(len(all_excs), len(eg)) + self.assertEqual(len(all_excs), len(match) + len(rest)) for e in all_excs: self.assertIn(e, eg) # every exception in all_excs is in eg and @@ -301,10 +301,94 @@ def test_split_nested(self): else: self.assertFalse(True, 'exception not caught') - class ExceptionGroupCatchTests(ExceptionGroupTestUtils): - pass # TODO - write the catch context manager and add tests + def test_catch_simple_eg_swallowing_handler(self): + checkMatch =self.assertMatchesTemplate + + def handler(eg): + nonlocal caught + caught = eg + + try: ######### Catch nothing: + caught = raised = None + with ExceptionGroup.catch(SyntaxError, handler): + self.simple_exception_group(7) + except ExceptionGroup as eg: + raised = eg + eg_template = self.get_test_exceptions_list(7) + checkMatch(raised, eg_template) + self.assertIsNone(caught) + + try: ######### Catch everything: + caught = None + with ExceptionGroup.catch((ValueError, TypeError), handler): + self.simple_exception_group(8) + finally: + eg_template = self.get_test_exceptions_list(8) + checkMatch(caught, eg_template) + + try: ######### Catch something: + caught = raised = None + with ExceptionGroup.catch(TypeError, handler): + self.simple_exception_group(6) + except ExceptionGroup as eg: + raised = eg + eg_template = self.get_test_exceptions_list(6) + checkMatch(raised, self._reduce(eg_template, ValueError)) + checkMatch(caught, self._reduce(eg_template, TypeError)) + + try: ######### Catch something: + caught = raised = None + with ExceptionGroup.catch((ValueError, SyntaxError), handler): + self.simple_exception_group(6) + except ExceptionGroup as eg: + raised = eg + eg_template = self.get_test_exceptions_list(6) + checkMatch(raised, self._reduce(eg_template, TypeError)) + checkMatch(caught, self._reduce(eg_template, ValueError)) + def test_catch_nested_eg_swallowing_handler(self): + checkMatch =self.assertMatchesTemplate + + eg_template = [self.get_test_exceptions_list(i) for i in [1,2,3]] + + def handler(eg): + nonlocal caught + caught = eg + + try: ######### Catch nothing: + caught = raised = None + with ExceptionGroup.catch(SyntaxError, handler): + self.nested_exception_group() + except ExceptionGroup as eg: + raised = eg + checkMatch(raised, eg_template) + self.assertIsNone(caught) + + try: ######### Catch everything: + caught = None + with ExceptionGroup.catch((ValueError, TypeError), handler): + self.nested_exception_group() + finally: + checkMatch(caught, eg_template) + + try: ######### Catch something: + caught = raised = None + with ExceptionGroup.catch(TypeError, handler): + self.nested_exception_group() + except ExceptionGroup as eg: + raised = eg + checkMatch(raised, self._reduce(eg_template, ValueError)) + checkMatch(caught, self._reduce(eg_template, TypeError)) + + try: ######### Catch something: + caught = raised = None + with ExceptionGroup.catch((ValueError, SyntaxError), handler): + self.nested_exception_group() + except ExceptionGroup as eg: + raised = eg + checkMatch(raised, self._reduce(eg_template, TypeError)) + checkMatch(caught, self._reduce(eg_template, ValueError)) if __name__ == '__main__': unittest.main() From 166415d92bebac4d20988527832d148c77c31bb1 Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Thu, 29 Oct 2020 17:47:34 +0000 Subject: [PATCH 19/73] tidy up tests --- Lib/test/test_exception_group.py | 68 ++++++++++++++++---------------- 1 file changed, 35 insertions(+), 33 deletions(-) diff --git a/Lib/test/test_exception_group.py b/Lib/test/test_exception_group.py index 6dd6ede17a0d16..9d12ca52902b81 100644 --- a/Lib/test/test_exception_group.py +++ b/Lib/test/test_exception_group.py @@ -226,6 +226,7 @@ def _split_exception_group(self, eg, types): return match, rest def test_split_simple(self): + checkMatch = self.assertMatchesTemplate try: self.simple_exception_group(5) self.assertFalse(True, 'exception not raised') @@ -234,36 +235,37 @@ def test_split_simple(self): self.assertEqual(self.funcnames(eg.__traceback__), fnames) eg_template = self.get_test_exceptions_list(5) - self.assertMatchesTemplate(eg, eg_template) + checkMatch(eg, eg_template) match, rest = self._split_exception_group(eg, SyntaxError) - self.assertMatchesTemplate(eg, eg_template) - self.assertMatchesTemplate(match, []) - self.assertMatchesTemplate(rest, eg_template) + checkMatch(eg, eg_template) + checkMatch(match, []) + checkMatch(rest, eg_template) match, rest = self._split_exception_group(eg, ValueError) - self.assertMatchesTemplate(eg, eg_template) - self.assertMatchesTemplate(match, [ValueError(i) for i in [6,7,8]]) - self.assertMatchesTemplate(rest, [TypeError(t) for t in ['int', 'list']]) + checkMatch(eg, eg_template) + checkMatch(match, self._reduce(eg_template, ValueError)) + checkMatch(rest, self._reduce(eg_template, TypeError)) match, rest = self._split_exception_group(eg, TypeError) - self.assertMatchesTemplate(eg, eg_template) - self.assertMatchesTemplate(match, [TypeError(t) for t in ['int', 'list']]) - self.assertMatchesTemplate(rest, [ValueError(i) for i in [6,7,8]]) + checkMatch(eg, eg_template) + checkMatch(match, self._reduce(eg_template, TypeError)) + checkMatch(rest, self._reduce(eg_template, ValueError)) match, rest = self._split_exception_group(eg, (ValueError, SyntaxError)) - self.assertMatchesTemplate(eg, eg_template) - self.assertMatchesTemplate(match, [ValueError(i) for i in [6,7,8]]) - self.assertMatchesTemplate(rest, [TypeError(t) for t in ['int', 'list']]) + checkMatch(eg, eg_template) + checkMatch(match, self._reduce(eg_template, ValueError)) + checkMatch(rest, self._reduce(eg_template, TypeError)) match, rest = self._split_exception_group(eg, (ValueError, TypeError)) - self.assertMatchesTemplate(eg, eg_template) - self.assertMatchesTemplate(match, eg_template) - self.assertMatchesTemplate(rest, []) + checkMatch(eg, eg_template) + checkMatch(match, eg_template) + checkMatch(rest, []) else: self.assertFalse(True, 'exception not caught') def test_split_nested(self): + checkMatch = self.assertMatchesTemplate try: self.nested_exception_group() self.assertFalse(True, 'exception not raised') @@ -272,38 +274,38 @@ def test_split_nested(self): self.assertEqual(self.funcnames(eg.__traceback__), fnames) eg_template = [self.get_test_exceptions_list(i) for i in [1,2,3]] - self.assertMatchesTemplate(eg, eg_template) + checkMatch(eg, eg_template) match, rest = self._split_exception_group(eg, SyntaxError) - self.assertMatchesTemplate(eg, eg_template) - self.assertMatchesTemplate(match, [[],[],[]]) - self.assertMatchesTemplate(rest, eg_template) + checkMatch(eg, eg_template) + checkMatch(match, [[],[],[]]) + checkMatch(rest, eg_template) match, rest = self._split_exception_group(eg, ValueError) - self.assertMatchesTemplate(eg, eg_template) - self.assertMatchesTemplate(match, self._reduce(eg_template, ValueError)) - self.assertMatchesTemplate(rest, self._reduce(eg_template, TypeError)) + checkMatch(eg, eg_template) + checkMatch(match, self._reduce(eg_template, ValueError)) + checkMatch(rest, self._reduce(eg_template, TypeError)) match, rest = self._split_exception_group(eg, TypeError) - self.assertMatchesTemplate(eg, eg_template) - self.assertMatchesTemplate(match, self._reduce(eg_template, TypeError)) - self.assertMatchesTemplate(rest, self._reduce(eg_template, ValueError)) + checkMatch(eg, eg_template) + checkMatch(match, self._reduce(eg_template, TypeError)) + checkMatch(rest, self._reduce(eg_template, ValueError)) match, rest = self._split_exception_group(eg, (ValueError, SyntaxError)) - self.assertMatchesTemplate(eg, eg_template) - self.assertMatchesTemplate(match, self._reduce(eg_template, ValueError)) - self.assertMatchesTemplate(rest, self._reduce(eg_template, TypeError)) + checkMatch(eg, eg_template) + checkMatch(match, self._reduce(eg_template, ValueError)) + checkMatch(rest, self._reduce(eg_template, TypeError)) match, rest = self._split_exception_group(eg, (ValueError, TypeError)) - self.assertMatchesTemplate(eg, eg_template) - self.assertMatchesTemplate(match, eg_template) - self.assertMatchesTemplate(rest, [[],[],[]]) + checkMatch(eg, eg_template) + checkMatch(match, eg_template) + checkMatch(rest, [[],[],[]]) else: self.assertFalse(True, 'exception not caught') class ExceptionGroupCatchTests(ExceptionGroupTestUtils): def test_catch_simple_eg_swallowing_handler(self): - checkMatch =self.assertMatchesTemplate + checkMatch = self.assertMatchesTemplate def handler(eg): nonlocal caught From a177e97b2f4e7bac1e9c0b8d019bc15ea1895470 Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Thu, 29 Oct 2020 18:17:21 +0000 Subject: [PATCH 20/73] added tests for catch with a handler that raises new exceptions (not checking tracebacks yet) --- Lib/test/test_exception_group.py | 54 ++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/Lib/test/test_exception_group.py b/Lib/test/test_exception_group.py index 9d12ca52902b81..31004f5c60a63c 100644 --- a/Lib/test/test_exception_group.py +++ b/Lib/test/test_exception_group.py @@ -392,5 +392,59 @@ def handler(eg): checkMatch(raised, self._reduce(eg_template, TypeError)) checkMatch(caught, self._reduce(eg_template, ValueError)) + def test_catch_nested_eg_raising_handler(self): + checkMatch =self.assertMatchesTemplate + + eg_template = [self.get_test_exceptions_list(i) for i in [1,2,3]] + raised_template = [ValueError('foo'), + [SyntaxError('bar'), ValueError('baz')] + ] + + def handler(eg): + nonlocal caught + caught = eg + return ExceptionGroup( + [ValueError('foo'), + ExceptionGroup( + [SyntaxError('bar'), ValueError('baz')])]) + + try: ######### Catch nothing: + caught = raised = None + with ExceptionGroup.catch(SyntaxError, handler): + self.nested_exception_group() + except ExceptionGroup as eg: + raised = eg + checkMatch(raised, eg_template) + self.assertIsNone(caught) + + try: ######### Catch everything: + caught = None + with ExceptionGroup.catch((ValueError, TypeError), handler): + self.nested_exception_group() + except ExceptionGroup as eg: + raised = eg + checkMatch(raised, raised_template) + checkMatch(caught, eg_template) + + try: ######### Catch something: + caught = raised = None + with ExceptionGroup.catch(TypeError, handler): + self.nested_exception_group() + except ExceptionGroup as eg: + raised = eg + checkMatch(raised, + self._reduce(eg_template, ValueError) + raised_template) + checkMatch(caught, self._reduce(eg_template, TypeError)) + + try: ######### Catch something: + caught = raised = None + with ExceptionGroup.catch((ValueError, SyntaxError), handler): + self.nested_exception_group() + except ExceptionGroup as eg: + raised = eg + checkMatch(raised, + self._reduce(eg_template, TypeError) + raised_template) + checkMatch(caught, self._reduce(eg_template, ValueError)) + if __name__ == '__main__': unittest.main() From 6d7829122079ad9c0ebb4f2f1a10e0850381eab0 Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Thu, 29 Oct 2020 18:20:51 +0000 Subject: [PATCH 21/73] tidy up tests --- Lib/test/test_exception_group.py | 249 +++++++++++++++---------------- 1 file changed, 122 insertions(+), 127 deletions(-) diff --git a/Lib/test/test_exception_group.py b/Lib/test/test_exception_group.py index 31004f5c60a63c..6d991416c80e21 100644 --- a/Lib/test/test_exception_group.py +++ b/Lib/test/test_exception_group.py @@ -117,72 +117,69 @@ def test_construction_simple(self): # create a simple exception group and check that # it is constructed as expected try: + eg = None self.simple_exception_group(0) - self.assertFalse(True, 'exception not raised') - except ExceptionGroup as eg: - # check eg.excs - self.assertIsInstance(eg.excs, collections.abc.Sequence) - self.assertMatchesTemplate(eg, self.get_test_exceptions_list(0)) - - # check iteration - self.assertEqual(list(eg), list(eg.excs)) - - # check eg.__traceback__ - self.assertEqual(self.funcnames(eg.__traceback__), - ['test_construction_simple', 'simple_exception_group']) - - # check eg.__traceback_group__ - tbg = eg.__traceback_group__ - self.assertEqual(len(tbg.tb_next_map), 5) - self.assertEqual(tbg.tb_next_map.keys(), set(eg.excs)) - for e in eg.excs: - tb = tbg.tb_next_map[e] - self.assertEqual(self.funcnames(tb), ['raise'+type(e).__name__]) - - else: - self.assertFalse(True, 'exception not caught') + except ExceptionGroup as e: + eg = e + # check eg.excs + self.assertIsInstance(eg.excs, collections.abc.Sequence) + self.assertMatchesTemplate(eg, self.get_test_exceptions_list(0)) + + # check iteration + self.assertEqual(list(eg), list(eg.excs)) + + # check eg.__traceback__ + self.assertEqual(self.funcnames(eg.__traceback__), + ['test_construction_simple', 'simple_exception_group']) + + # check eg.__traceback_group__ + tbg = eg.__traceback_group__ + self.assertEqual(len(tbg.tb_next_map), 5) + self.assertEqual(tbg.tb_next_map.keys(), set(eg.excs)) + for e in eg.excs: + tb = tbg.tb_next_map[e] + self.assertEqual(self.funcnames(tb), ['raise'+type(e).__name__]) def test_construction_nested(self): # create a nested exception group and check that # it is constructed as expected try: + eg = None self.nested_exception_group() - self.assertFalse(True, 'exception not raised') - except ExceptionGroup as eg: - # check eg.excs - self.assertIsInstance(eg.excs, collections.abc.Sequence) - self.assertEqual(len(eg.excs), 3) - - # each of eg.excs is an EG with 3xValueError and 2xTypeErrors - all_excs = [] - for e in eg.excs: - self.assertIsInstance(e, ExceptionGroup) - self.assertEqual(len(e.excs), 5) - etypes = [type(e) for e in e.excs] - self.assertEqual(etypes.count(ValueError), 3) - self.assertEqual(etypes.count(TypeError), 2) - all_excs.extend(e.excs) - - eg_template = [self.get_test_exceptions_list(i) for i in [1,2,3]] - self.assertMatchesTemplate(eg, eg_template) - - # check iteration - self.assertEqual(list(eg), all_excs) - - # check eg.__traceback__ - self.assertEqual(self.funcnames(eg.__traceback__), - ['test_construction_nested', 'nested_exception_group']) - - # check eg.__traceback_group__ - tbg = eg.__traceback_group__ - self.assertEqual(len(tbg.tb_next_map), 15) - self.assertEqual(list(tbg.tb_next_map.keys()), all_excs) - for e in all_excs: - tb = tbg.tb_next_map[e] - self.assertEqual(self.funcnames(tb), - ['simple_exception_group', 'raise'+type(e).__name__]) - else: - self.assertFalse(True, 'exception not caught') + except ExceptionGroup as e: + eg = e + # check eg.excs + self.assertIsInstance(eg.excs, collections.abc.Sequence) + self.assertEqual(len(eg.excs), 3) + + # each of eg.excs is an EG with 3xValueError and 2xTypeErrors + all_excs = [] + for e in eg.excs: + self.assertIsInstance(e, ExceptionGroup) + self.assertEqual(len(e.excs), 5) + etypes = [type(e) for e in e.excs] + self.assertEqual(etypes.count(ValueError), 3) + self.assertEqual(etypes.count(TypeError), 2) + all_excs.extend(e.excs) + + eg_template = [self.get_test_exceptions_list(i) for i in [1,2,3]] + self.assertMatchesTemplate(eg, eg_template) + + # check iteration + self.assertEqual(list(eg), all_excs) + + # check eg.__traceback__ + self.assertEqual(self.funcnames(eg.__traceback__), + ['test_construction_nested', 'nested_exception_group']) + + # check eg.__traceback_group__ + tbg = eg.__traceback_group__ + self.assertEqual(len(tbg.tb_next_map), 15) + self.assertEqual(list(tbg.tb_next_map.keys()), all_excs) + for e in all_excs: + tb = tbg.tb_next_map[e] + self.assertEqual(self.funcnames(tb), + ['simple_exception_group', 'raise'+type(e).__name__]) class ExceptionGroupSplitTests(ExceptionGroupTestUtils): def _check_traceback_group_after_split(self, source_eg, eg): @@ -228,80 +225,78 @@ def _split_exception_group(self, eg, types): def test_split_simple(self): checkMatch = self.assertMatchesTemplate try: + eg = None self.simple_exception_group(5) - self.assertFalse(True, 'exception not raised') - except ExceptionGroup as eg: - fnames = ['test_split_simple', 'simple_exception_group'] - self.assertEqual(self.funcnames(eg.__traceback__), fnames) - - eg_template = self.get_test_exceptions_list(5) - checkMatch(eg, eg_template) - - match, rest = self._split_exception_group(eg, SyntaxError) - checkMatch(eg, eg_template) - checkMatch(match, []) - checkMatch(rest, eg_template) - - match, rest = self._split_exception_group(eg, ValueError) - checkMatch(eg, eg_template) - checkMatch(match, self._reduce(eg_template, ValueError)) - checkMatch(rest, self._reduce(eg_template, TypeError)) - - match, rest = self._split_exception_group(eg, TypeError) - checkMatch(eg, eg_template) - checkMatch(match, self._reduce(eg_template, TypeError)) - checkMatch(rest, self._reduce(eg_template, ValueError)) - - match, rest = self._split_exception_group(eg, (ValueError, SyntaxError)) - checkMatch(eg, eg_template) - checkMatch(match, self._reduce(eg_template, ValueError)) - checkMatch(rest, self._reduce(eg_template, TypeError)) - - match, rest = self._split_exception_group(eg, (ValueError, TypeError)) - checkMatch(eg, eg_template) - checkMatch(match, eg_template) - checkMatch(rest, []) - else: - self.assertFalse(True, 'exception not caught') + except ExceptionGroup as e: + eg = e + fnames = ['test_split_simple', 'simple_exception_group'] + self.assertEqual(self.funcnames(eg.__traceback__), fnames) + + eg_template = self.get_test_exceptions_list(5) + checkMatch(eg, eg_template) + + match, rest = self._split_exception_group(eg, SyntaxError) + checkMatch(eg, eg_template) + checkMatch(match, []) + checkMatch(rest, eg_template) + + match, rest = self._split_exception_group(eg, ValueError) + checkMatch(eg, eg_template) + checkMatch(match, self._reduce(eg_template, ValueError)) + checkMatch(rest, self._reduce(eg_template, TypeError)) + + match, rest = self._split_exception_group(eg, TypeError) + checkMatch(eg, eg_template) + checkMatch(match, self._reduce(eg_template, TypeError)) + checkMatch(rest, self._reduce(eg_template, ValueError)) + + match, rest = self._split_exception_group(eg, (ValueError, SyntaxError)) + checkMatch(eg, eg_template) + checkMatch(match, self._reduce(eg_template, ValueError)) + checkMatch(rest, self._reduce(eg_template, TypeError)) + + match, rest = self._split_exception_group(eg, (ValueError, TypeError)) + checkMatch(eg, eg_template) + checkMatch(match, eg_template) + checkMatch(rest, []) def test_split_nested(self): checkMatch = self.assertMatchesTemplate try: + eg = None self.nested_exception_group() - self.assertFalse(True, 'exception not raised') - except ExceptionGroup as eg: - fnames = ['test_split_nested', 'nested_exception_group'] - self.assertEqual(self.funcnames(eg.__traceback__), fnames) - - eg_template = [self.get_test_exceptions_list(i) for i in [1,2,3]] - checkMatch(eg, eg_template) - - match, rest = self._split_exception_group(eg, SyntaxError) - checkMatch(eg, eg_template) - checkMatch(match, [[],[],[]]) - checkMatch(rest, eg_template) - - match, rest = self._split_exception_group(eg, ValueError) - checkMatch(eg, eg_template) - checkMatch(match, self._reduce(eg_template, ValueError)) - checkMatch(rest, self._reduce(eg_template, TypeError)) - - match, rest = self._split_exception_group(eg, TypeError) - checkMatch(eg, eg_template) - checkMatch(match, self._reduce(eg_template, TypeError)) - checkMatch(rest, self._reduce(eg_template, ValueError)) - - match, rest = self._split_exception_group(eg, (ValueError, SyntaxError)) - checkMatch(eg, eg_template) - checkMatch(match, self._reduce(eg_template, ValueError)) - checkMatch(rest, self._reduce(eg_template, TypeError)) - - match, rest = self._split_exception_group(eg, (ValueError, TypeError)) - checkMatch(eg, eg_template) - checkMatch(match, eg_template) - checkMatch(rest, [[],[],[]]) - else: - self.assertFalse(True, 'exception not caught') + except ExceptionGroup as e: + eg = e + fnames = ['test_split_nested', 'nested_exception_group'] + self.assertEqual(self.funcnames(eg.__traceback__), fnames) + + eg_template = [self.get_test_exceptions_list(i) for i in [1,2,3]] + checkMatch(eg, eg_template) + + match, rest = self._split_exception_group(eg, SyntaxError) + checkMatch(eg, eg_template) + checkMatch(match, [[],[],[]]) + checkMatch(rest, eg_template) + + match, rest = self._split_exception_group(eg, ValueError) + checkMatch(eg, eg_template) + checkMatch(match, self._reduce(eg_template, ValueError)) + checkMatch(rest, self._reduce(eg_template, TypeError)) + + match, rest = self._split_exception_group(eg, TypeError) + checkMatch(eg, eg_template) + checkMatch(match, self._reduce(eg_template, TypeError)) + checkMatch(rest, self._reduce(eg_template, ValueError)) + + match, rest = self._split_exception_group(eg, (ValueError, SyntaxError)) + checkMatch(eg, eg_template) + checkMatch(match, self._reduce(eg_template, ValueError)) + checkMatch(rest, self._reduce(eg_template, TypeError)) + + match, rest = self._split_exception_group(eg, (ValueError, TypeError)) + checkMatch(eg, eg_template) + checkMatch(match, eg_template) + checkMatch(rest, [[],[],[]]) class ExceptionGroupCatchTests(ExceptionGroupTestUtils): def test_catch_simple_eg_swallowing_handler(self): From 8ff067a2e671535646669e90eb5d60add36256f4 Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Thu, 29 Oct 2020 18:43:48 +0000 Subject: [PATCH 22/73] handle case where EG is created with an exception with no traceback (was not raised yet) --- Lib/exception_group.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Lib/exception_group.py b/Lib/exception_group.py index 4480a52cd094b6..f578cf2fd34989 100644 --- a/Lib/exception_group.py +++ b/Lib/exception_group.py @@ -13,7 +13,10 @@ def __init__(self, excs): for e_ in e.excs: self.tb_next_map[e_] = e_.__traceback__ else: - self.tb_next_map[e] = e.__traceback__.tb_next + if e.__traceback__: + self.tb_next_map[e] = e.__traceback__.tb_next + else: + self.tb_next_map[e] = None class ExceptionGroup(BaseException): From e22bd01548b981202a5ab5384c657a733d7f3734 Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Thu, 29 Oct 2020 23:10:40 +0000 Subject: [PATCH 23/73] added ExceptionGroup.extract_traceback to get the traceback of a single exception from the group. Use it to simplify the tests --- Lib/exception_group.py | 31 ++++++++++++++ Lib/test/test_exception_group.py | 70 +++++++++++++++----------------- 2 files changed, 64 insertions(+), 37 deletions(-) diff --git a/Lib/exception_group.py b/Lib/exception_group.py index f578cf2fd34989..09b3866a7ca000 100644 --- a/Lib/exception_group.py +++ b/Lib/exception_group.py @@ -56,6 +56,37 @@ def push_frame(self, frame): self.__traceback__ = types.TracebackType( self.__traceback__, frame, 0, 0) + def extract_traceback(self, exc): + """ returns the traceback of a single exception + + If exc is in this exception group, return its + traceback as a list of frames. Otherwise, return None. + + Note: The frame where an exception was caught and + rereaised as part of an exception group appreas twice. + """ + if exc not in self: + return None + result = [] + tb = self.__traceback__ + while tb: + result.append(tb.tb_frame) + tb = tb.tb_next + next_e = None + for e in self.excs: + if exc == e or (isinstance(e, ExceptionGroup) and exc in e): + assert next_e is None + next_e = e + assert next_e is not None + if isinstance(next_e, ExceptionGroup): + result.extend(next_e.extract_traceback(exc)) + else: + tb = next_e.__traceback__ + while tb: + result.append(tb.tb_frame) + tb = tb.tb_next + return result + @staticmethod def render(exc, tb=None, indent=0): print(exc) diff --git a/Lib/test/test_exception_group.py b/Lib/test/test_exception_group.py index 6d991416c80e21..69b41bb8a5ce89 100644 --- a/Lib/test/test_exception_group.py +++ b/Lib/test/test_exception_group.py @@ -55,12 +55,15 @@ def nested_exception_group(self): excs.append(e) raise ExceptionGroup(excs) + def funcname(self, tb_frame): + return tb_frame.f_code.co_name + def funcnames(self, tb): """ Extract function names from a traceback """ - funcname = lambda tb_frame: tb_frame.f_code.co_name + names = [] while tb: - names.append(funcname(tb.tb_frame)) + names.append(self.funcname(tb.tb_frame)) tb = tb.tb_next return names @@ -128,17 +131,16 @@ def test_construction_simple(self): # check iteration self.assertEqual(list(eg), list(eg.excs)) - # check eg.__traceback__ - self.assertEqual(self.funcnames(eg.__traceback__), - ['test_construction_simple', 'simple_exception_group']) - - # check eg.__traceback_group__ - tbg = eg.__traceback_group__ - self.assertEqual(len(tbg.tb_next_map), 5) - self.assertEqual(tbg.tb_next_map.keys(), set(eg.excs)) - for e in eg.excs: - tb = tbg.tb_next_map[e] - self.assertEqual(self.funcnames(tb), ['raise'+type(e).__name__]) + # check tracebacks + for e in eg: + expected = [ + 'test_construction_simple', + 'simple_exception_group', + 'simple_exception_group', + 'raise'+type(e).__name__, + ] + etb = eg.extract_traceback(e) + self.assertEqual(expected, [self.funcname(f) for f in etb]) def test_construction_nested(self): # create a nested exception group and check that @@ -168,27 +170,20 @@ def test_construction_nested(self): # check iteration self.assertEqual(list(eg), all_excs) - # check eg.__traceback__ - self.assertEqual(self.funcnames(eg.__traceback__), - ['test_construction_nested', 'nested_exception_group']) - - # check eg.__traceback_group__ - tbg = eg.__traceback_group__ - self.assertEqual(len(tbg.tb_next_map), 15) - self.assertEqual(list(tbg.tb_next_map.keys()), all_excs) - for e in all_excs: - tb = tbg.tb_next_map[e] - self.assertEqual(self.funcnames(tb), - ['simple_exception_group', 'raise'+type(e).__name__]) + # check tracebacks + for e in eg: + expected = [ + 'test_construction_nested', + 'nested_exception_group', + 'nested_exception_group', + 'simple_exception_group', + 'simple_exception_group', + 'raise'+type(e).__name__, + ] + etb = eg.extract_traceback(e) + self.assertEqual(expected, [self.funcname(f) for f in etb]) class ExceptionGroupSplitTests(ExceptionGroupTestUtils): - def _check_traceback_group_after_split(self, source_eg, eg): - tb_next_map = eg.__traceback_group__.tb_next_map - source_tb_next_map = source_eg.__traceback_group__.tb_next_map - for e in eg: - self.assertEqual(self.funcnames(tb_next_map[e]), - self.funcnames(source_tb_next_map[e])) - self.assertEqual(len(tb_next_map), len(eg)) def _split_exception_group(self, eg, types): """ Split an EG and do some sanity checks on the result """ @@ -214,12 +209,13 @@ def _split_exception_group(self, eg, types): for e in rest: self.assertNotIsInstance(e, types) - # traceback was copied over - self.assertEqual(self.funcnames(match.__traceback__), fnames) - self.assertEqual(self.funcnames(rest.__traceback__), fnames) + # check tracebacks + for part in [match, rest]: + for e in part: + self.assertEqual( + eg.extract_traceback(e), + part.extract_traceback(e)) - self._check_traceback_group_after_split(eg, match) - self._check_traceback_group_after_split(eg, rest) return match, rest def test_split_simple(self): From 2a6673c8b9ab6be840cc5919e2aca158849048e3 Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Fri, 30 Oct 2020 00:24:19 +0000 Subject: [PATCH 24/73] added traceback checks to the Cacther tests. Fixed bug in Catcher's merging of new and old exceptions --- Lib/exception_group.py | 4 +- Lib/test/test_exception_group.py | 116 ++++++++++++++++++++----------- 2 files changed, 78 insertions(+), 42 deletions(-) diff --git a/Lib/exception_group.py b/Lib/exception_group.py index 09b3866a7ca000..51c8bafb1f7957 100644 --- a/Lib/exception_group.py +++ b/Lib/exception_group.py @@ -166,9 +166,7 @@ def __exit__(self, etype, exc, tb): else: # merge rest and new_exceptions # keep the traceback from rest - to_raise = ExceptionGroup( - rest.excs + new_exception_group.excs, - tb = rest.__traceback__) + to_raise = ExceptionGroup([rest, new_exception_group]) # When we raise to_raise, Python will unconditionally blow # away its __context__ attribute and replace it with the original diff --git a/Lib/test/test_exception_group.py b/Lib/test/test_exception_group.py index 69b41bb8a5ce89..29c379ceda63a5 100644 --- a/Lib/test/test_exception_group.py +++ b/Lib/test/test_exception_group.py @@ -295,8 +295,29 @@ def test_split_nested(self): checkMatch(rest, [[],[],[]]) class ExceptionGroupCatchTests(ExceptionGroupTestUtils): + def checkMatch(self, exc, template, reference_tbs): + self.assertMatchesTemplate(exc, template) + for e in exc: + result = [self.funcname(f) for f in exc.extract_traceback(e)] + ref = reference_tbs[(type(e), e.args)] + # result has more frames from the Catcher context + # manager, ignore them + try: + result.remove('__exit__') + except ValueError: + pass + if result != ref: + self.assertEqual(result[-len(ref):], ref) + def test_catch_simple_eg_swallowing_handler(self): - checkMatch = self.assertMatchesTemplate + try: + self.simple_exception_group(12) + except ExceptionGroup as eg: + ref_tbs = {} + for e in eg: + tb = [self.funcname(f) for f in eg.extract_traceback(e)] + ref_tbs[(type(e), e.args)] = tb + eg_template = self.get_test_exceptions_list(12) def handler(eg): nonlocal caught @@ -305,44 +326,45 @@ def handler(eg): try: ######### Catch nothing: caught = raised = None with ExceptionGroup.catch(SyntaxError, handler): - self.simple_exception_group(7) + self.simple_exception_group(12) except ExceptionGroup as eg: raised = eg - eg_template = self.get_test_exceptions_list(7) - checkMatch(raised, eg_template) + self.checkMatch(raised, eg_template, ref_tbs) self.assertIsNone(caught) try: ######### Catch everything: caught = None with ExceptionGroup.catch((ValueError, TypeError), handler): - self.simple_exception_group(8) + self.simple_exception_group(12) finally: - eg_template = self.get_test_exceptions_list(8) - checkMatch(caught, eg_template) + self.checkMatch(caught, eg_template, ref_tbs) try: ######### Catch something: caught = raised = None with ExceptionGroup.catch(TypeError, handler): - self.simple_exception_group(6) + self.simple_exception_group(12) except ExceptionGroup as eg: raised = eg - eg_template = self.get_test_exceptions_list(6) - checkMatch(raised, self._reduce(eg_template, ValueError)) - checkMatch(caught, self._reduce(eg_template, TypeError)) + self.checkMatch(raised, self._reduce(eg_template, ValueError), ref_tbs) + self.checkMatch(caught, self._reduce(eg_template, TypeError), ref_tbs) try: ######### Catch something: caught = raised = None with ExceptionGroup.catch((ValueError, SyntaxError), handler): - self.simple_exception_group(6) + self.simple_exception_group(12) except ExceptionGroup as eg: raised = eg - eg_template = self.get_test_exceptions_list(6) - checkMatch(raised, self._reduce(eg_template, TypeError)) - checkMatch(caught, self._reduce(eg_template, ValueError)) + self.checkMatch(raised, self._reduce(eg_template, TypeError), ref_tbs) + self.checkMatch(caught, self._reduce(eg_template, ValueError), ref_tbs) def test_catch_nested_eg_swallowing_handler(self): - checkMatch =self.assertMatchesTemplate - + try: + self.nested_exception_group() + except ExceptionGroup as eg: + ref_tbs = {} + for e in eg: + tb = [self.funcname(f) for f in eg.extract_traceback(e)] + ref_tbs[(type(e), e.args)] = tb eg_template = [self.get_test_exceptions_list(i) for i in [1,2,3]] def handler(eg): @@ -355,7 +377,7 @@ def handler(eg): self.nested_exception_group() except ExceptionGroup as eg: raised = eg - checkMatch(raised, eg_template) + self.checkMatch(raised, eg_template, ref_tbs) self.assertIsNone(caught) try: ######### Catch everything: @@ -363,7 +385,7 @@ def handler(eg): with ExceptionGroup.catch((ValueError, TypeError), handler): self.nested_exception_group() finally: - checkMatch(caught, eg_template) + self.checkMatch(caught, eg_template, ref_tbs) try: ######### Catch something: caught = raised = None @@ -371,8 +393,8 @@ def handler(eg): self.nested_exception_group() except ExceptionGroup as eg: raised = eg - checkMatch(raised, self._reduce(eg_template, ValueError)) - checkMatch(caught, self._reduce(eg_template, TypeError)) + self.checkMatch(raised, self._reduce(eg_template, ValueError), ref_tbs) + self.checkMatch(caught, self._reduce(eg_template, TypeError), ref_tbs) try: ######### Catch something: caught = raised = None @@ -380,17 +402,10 @@ def handler(eg): self.nested_exception_group() except ExceptionGroup as eg: raised = eg - checkMatch(raised, self._reduce(eg_template, TypeError)) - checkMatch(caught, self._reduce(eg_template, ValueError)) + self.checkMatch(raised, self._reduce(eg_template, TypeError), ref_tbs) + self.checkMatch(caught, self._reduce(eg_template, ValueError), ref_tbs) def test_catch_nested_eg_raising_handler(self): - checkMatch =self.assertMatchesTemplate - - eg_template = [self.get_test_exceptions_list(i) for i in [1,2,3]] - raised_template = [ValueError('foo'), - [SyntaxError('bar'), ValueError('baz')] - ] - def handler(eg): nonlocal caught caught = eg @@ -399,13 +414,34 @@ def handler(eg): ExceptionGroup( [SyntaxError('bar'), ValueError('baz')])]) + try: + self.nested_exception_group() + except ExceptionGroup as eg: + eg1 = eg + try: + raise handler(None) + except ExceptionGroup as eg: + eg2 = eg + + ref_tbs = {} + for eg in (eg1, eg2): + for e in eg: + tb = [self.funcname(f) for f in eg.extract_traceback(e)] + ref_tbs[(type(e), e.args)] = tb + + eg_template = [self.get_test_exceptions_list(i) for i in [1,2,3]] + + raised_template = [ValueError('foo'), + [SyntaxError('bar'), ValueError('baz')]] + + try: ######### Catch nothing: caught = raised = None with ExceptionGroup.catch(SyntaxError, handler): self.nested_exception_group() except ExceptionGroup as eg: raised = eg - checkMatch(raised, eg_template) + self.checkMatch(raised, eg_template, ref_tbs) self.assertIsNone(caught) try: ######### Catch everything: @@ -414,8 +450,8 @@ def handler(eg): self.nested_exception_group() except ExceptionGroup as eg: raised = eg - checkMatch(raised, raised_template) - checkMatch(caught, eg_template) + self.checkMatch(raised, raised_template, ref_tbs) + self.checkMatch(caught, eg_template, ref_tbs) try: ######### Catch something: caught = raised = None @@ -423,9 +459,10 @@ def handler(eg): self.nested_exception_group() except ExceptionGroup as eg: raised = eg - checkMatch(raised, - self._reduce(eg_template, ValueError) + raised_template) - checkMatch(caught, self._reduce(eg_template, TypeError)) + self.checkMatch(raised, + [self._reduce(eg_template, ValueError), raised_template], + ref_tbs) + self.checkMatch(caught, self._reduce(eg_template, TypeError), ref_tbs) try: ######### Catch something: caught = raised = None @@ -433,9 +470,10 @@ def handler(eg): self.nested_exception_group() except ExceptionGroup as eg: raised = eg - checkMatch(raised, - self._reduce(eg_template, TypeError) + raised_template) - checkMatch(caught, self._reduce(eg_template, ValueError)) + self.checkMatch(raised, + [self._reduce(eg_template, TypeError), raised_template], + ref_tbs) + self.checkMatch(caught, self._reduce(eg_template, ValueError), ref_tbs) if __name__ == '__main__': unittest.main() From ddf4677b2ccbea80a3b56fee79aed613d647442a Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Fri, 30 Oct 2020 01:04:41 +0000 Subject: [PATCH 25/73] derive template from the exception in the tests --- Lib/test/test_exception_group.py | 77 +++++++++++++++++++++----------- 1 file changed, 52 insertions(+), 25 deletions(-) diff --git a/Lib/test/test_exception_group.py b/Lib/test/test_exception_group.py index 29c379ceda63a5..a7a7e975609297 100644 --- a/Lib/test/test_exception_group.py +++ b/Lib/test/test_exception_group.py @@ -18,6 +18,12 @@ def assertMatchesTemplate(self, exc, template): self.assertEqual(type(exc), type(template)) self.assertEqual(exc.args, template.args) + def to_template(self, exc): + if isinstance(exc, ExceptionGroup): + return [self.to_template(e) for e in exc.excs] + else: + return exc + class ExceptionGroupTestUtils(ExceptionGroupTestBase): def raiseValueError(self, v): raise ValueError(v) @@ -34,9 +40,6 @@ def get_test_exceptions(self, x): (self.raiseTypeError, TypeError, 'list'), ] - def get_test_exceptions_list(self, x): - return [t(arg) for _, t, arg in self.get_test_exceptions(x)] - def simple_exception_group(self, x): excs = [] for f, _, arg in self.get_test_exceptions(x): @@ -60,7 +63,6 @@ def funcname(self, tb_frame): def funcnames(self, tb): """ Extract function names from a traceback """ - names = [] while tb: names.append(self.funcname(tb.tb_frame)) @@ -88,15 +90,43 @@ def test_basic_utility_functions(self): self.assertRaises(ExceptionGroup, self.simple_exception_group, 42) self.assertRaises(ExceptionGroup, self.nested_exception_group) - test_excs = self.get_test_exceptions_list(42) - self.assertEqual(len(test_excs), 5) - expected = [("TypeError", 'int'), - ("TypeError", 'list'), - ("ValueError", 43), - ("ValueError", 44), - ("ValueError", 45)] - self.assertSequenceEqual(expected, - sorted((type(e).__name__, e.args[0]) for e in test_excs)) + try: + self.simple_exception_group(42) + except ExceptionGroup as eg: + template = self.to_template(eg) + self.assertEqual(len(template), 5) + expected = [ValueError(43), + TypeError('int'), + ValueError(44), + ValueError(45), + TypeError('list'), + ] + self.assertEqual(str(expected), str(template)) + + try: + self.nested_exception_group() + except ExceptionGroup as eg: + template = self.to_template(eg) + self.assertEqual(len(template), 3) + expected = [[ValueError(2), + TypeError('int'), + ValueError(3), + ValueError(4), + TypeError('list'), + ], + [ValueError(3), + TypeError('int'), + ValueError(4), + ValueError(5), + TypeError('list'), + ], + [ValueError(4), + TypeError('int'), + ValueError(5), + ValueError(6), + TypeError('list'), + ]] + self.assertEqual(str(expected), str(template)) def test_reduce(self): te = TypeError('int') @@ -126,7 +156,7 @@ def test_construction_simple(self): eg = e # check eg.excs self.assertIsInstance(eg.excs, collections.abc.Sequence) - self.assertMatchesTemplate(eg, self.get_test_exceptions_list(0)) + self.assertMatchesTemplate(eg, self.to_template(eg)) # check iteration self.assertEqual(list(eg), list(eg.excs)) @@ -164,7 +194,7 @@ def test_construction_nested(self): self.assertEqual(etypes.count(TypeError), 2) all_excs.extend(e.excs) - eg_template = [self.get_test_exceptions_list(i) for i in [1,2,3]] + eg_template = self.to_template(eg) self.assertMatchesTemplate(eg, eg_template) # check iteration @@ -228,7 +258,7 @@ def test_split_simple(self): fnames = ['test_split_simple', 'simple_exception_group'] self.assertEqual(self.funcnames(eg.__traceback__), fnames) - eg_template = self.get_test_exceptions_list(5) + eg_template = self.to_template(eg) checkMatch(eg, eg_template) match, rest = self._split_exception_group(eg, SyntaxError) @@ -266,7 +296,7 @@ def test_split_nested(self): fnames = ['test_split_nested', 'nested_exception_group'] self.assertEqual(self.funcnames(eg.__traceback__), fnames) - eg_template = [self.get_test_exceptions_list(i) for i in [1,2,3]] + eg_template = self.to_template(eg) checkMatch(eg, eg_template) match, rest = self._split_exception_group(eg, SyntaxError) @@ -317,7 +347,7 @@ def test_catch_simple_eg_swallowing_handler(self): for e in eg: tb = [self.funcname(f) for f in eg.extract_traceback(e)] ref_tbs[(type(e), e.args)] = tb - eg_template = self.get_test_exceptions_list(12) + eg_template = self.to_template(eg) def handler(eg): nonlocal caught @@ -365,7 +395,7 @@ def test_catch_nested_eg_swallowing_handler(self): for e in eg: tb = [self.funcname(f) for f in eg.extract_traceback(e)] ref_tbs[(type(e), e.args)] = tb - eg_template = [self.get_test_exceptions_list(i) for i in [1,2,3]] + eg_template = self.to_template(eg) def handler(eg): nonlocal caught @@ -418,10 +448,13 @@ def handler(eg): self.nested_exception_group() except ExceptionGroup as eg: eg1 = eg + eg_template = self.to_template(eg) + try: raise handler(None) except ExceptionGroup as eg: eg2 = eg + raised_template = self.to_template(eg) ref_tbs = {} for eg in (eg1, eg2): @@ -429,12 +462,6 @@ def handler(eg): tb = [self.funcname(f) for f in eg.extract_traceback(e)] ref_tbs[(type(e), e.args)] = tb - eg_template = [self.get_test_exceptions_list(i) for i in [1,2,3]] - - raised_template = [ValueError('foo'), - [SyntaxError('bar'), ValueError('baz')]] - - try: ######### Catch nothing: caught = raised = None with ExceptionGroup.catch(SyntaxError, handler): From 4d66d919ac7a45d53450ea0079f3000f039b445f Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Fri, 30 Oct 2020 13:35:04 +0000 Subject: [PATCH 26/73] added ExceptionGroup.subgroup() and use it in Catcher to preserve structure of the original exception --- Lib/exception_group.py | 64 ++++++++++++++++++++++---------- Lib/test/test_exception_group.py | 2 +- 2 files changed, 45 insertions(+), 21 deletions(-) diff --git a/Lib/exception_group.py b/Lib/exception_group.py index 51c8bafb1f7957..ff5473a6e3f0cb 100644 --- a/Lib/exception_group.py +++ b/Lib/exception_group.py @@ -28,22 +28,15 @@ def __init__(self, excs, tb=None): self.__traceback__ = tb self.__traceback_group__ = TracebackGroup(self.excs) - def split(self, E): - """Split an ExceptionGroup to extract exceptions matching E - - returns two new ExceptionGroups: match, rest of the exceptions of - self that match E and those that don't. - match and rest have the same nested structure as self. - E can be a type or tuple of types. - """ + def _split_on_condition(self, condition): match, rest = [], [] for e in self.excs: if isinstance(e, ExceptionGroup): # recurse - e_match, e_rest = e.split(E) + e_match, e_rest = e._split_on_condition(condition) match.append(e_match) rest.append(e_rest) else: - if isinstance(e, E): + if condition(e): match.append(e) e_match, e_rest = e, None else: @@ -51,6 +44,25 @@ def split(self, E): return (ExceptionGroup(match, tb=self.__traceback__), ExceptionGroup(rest, tb=self.__traceback__)) + def split(self, E): + """ Split an ExceptionGroup to extract exceptions matching E + + returns two new ExceptionGroups: match, rest of the exceptions of + self that match E and those that don't. + match and rest have the same nested structure as self. + E can be a type or tuple of types. + """ + return self._split_on_condition(lambda e: isinstance(e, E)) + + def subgroup(self, keep): + """ Return a subgroup of self including only the exception in keep + + returns a new ExceptionGroups that contains only the exception in + the sequence keep and preserves the internal structure of self. + """ + match, _ = self._split_on_condition(lambda e: e in keep) + return match + def push_frame(self, frame): import types self.__traceback__ = types.TracebackType( @@ -148,25 +160,37 @@ def __enter__(self): def __exit__(self, etype, exc, tb): if exc is not None and isinstance(exc, ExceptionGroup): - match, rest = exc.split(self.types) + match, unmatched = exc.split(self.types) if not match: # Let the interpreter reraise the exception return False - new_exception_group = self.handler(match) - if not new_exception_group and not rest: + handler_excs = self.handler(match) + if handler_excs == match: + # handler reraised all of the matched exceptions. + # reraise exc as is. + return False + + if not handler_excs and not unmatched: # handled and swallowed all exceptions + # do not raise anything. return True - if not new_exception_group: - to_raise = rest - elif not rest: - to_raise = new_exception_group + if not unmatched: + to_raise = handler_excs # raise what handler returned + elif not handler_excs: + to_raise = unmatched # raise the unmatched exceptions else: - # merge rest and new_exceptions - # keep the traceback from rest - to_raise = ExceptionGroup([rest, new_exception_group]) + # to_keep: EG subgroup of exc with only those to reraise + # (either not matched or reraised by handler) + to_keep = exc.subgroup( + list(unmatched) + [e for e in handler_excs if e in match]) + # to_add: new exceptions raised by handler + to_add = handler_excs.subgroup( + [e for e in handler_excs if e not in match]) + + to_raise = ExceptionGroup([to_keep, to_add]) # When we raise to_raise, Python will unconditionally blow # away its __context__ attribute and replace it with the original diff --git a/Lib/test/test_exception_group.py b/Lib/test/test_exception_group.py index a7a7e975609297..eccc828c22f975 100644 --- a/Lib/test/test_exception_group.py +++ b/Lib/test/test_exception_group.py @@ -435,7 +435,7 @@ def handler(eg): self.checkMatch(raised, self._reduce(eg_template, TypeError), ref_tbs) self.checkMatch(caught, self._reduce(eg_template, ValueError), ref_tbs) - def test_catch_nested_eg_raising_handler(self): + def test_catch_nested_eg_handler_raises_new_exceptions(self): def handler(eg): nonlocal caught caught = eg From e7bb3be8330e9fe92d0f83548bbb71c50d77a33b Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Fri, 30 Oct 2020 16:19:52 +0000 Subject: [PATCH 27/73] added more interesting catch tests. removed empty nested EGs from split and subgroup's output --- Lib/exception_group.py | 21 ++--- Lib/test/test_exception_group.py | 137 ++++++++++++++++++++++++++++++- 2 files changed, 146 insertions(+), 12 deletions(-) diff --git a/Lib/exception_group.py b/Lib/exception_group.py index ff5473a6e3f0cb..3c931feb806d39 100644 --- a/Lib/exception_group.py +++ b/Lib/exception_group.py @@ -20,7 +20,7 @@ def __init__(self, excs): class ExceptionGroup(BaseException): - def __init__(self, excs, tb=None): + def __init__(self, excs, *, tb=None): self.excs = excs # self.__traceback__ is updated as usual, but self.__traceback_group__ # is set when the exception group is created. @@ -33,8 +33,10 @@ def _split_on_condition(self, condition): for e in self.excs: if isinstance(e, ExceptionGroup): # recurse e_match, e_rest = e._split_on_condition(condition) - match.append(e_match) - rest.append(e_rest) + if e_match: + match.append(e_match) + if e_rest: + rest.append(e_rest) else: if condition(e): match.append(e) @@ -149,7 +151,7 @@ def __init__(self, types, handler): handler: a function that takes an ExceptionGroup of the matched type and does something with them - Any unmatched exceptions are raised at the end as another + Any rest exceptions are raised at the end as another exception group """ self.types = types @@ -160,7 +162,7 @@ def __enter__(self): def __exit__(self, etype, exc, tb): if exc is not None and isinstance(exc, ExceptionGroup): - match, unmatched = exc.split(self.types) + match, rest = exc.split(self.types) if not match: # Let the interpreter reraise the exception @@ -172,24 +174,23 @@ def __exit__(self, etype, exc, tb): # reraise exc as is. return False - if not handler_excs and not unmatched: + if not handler_excs and not rest: # handled and swallowed all exceptions # do not raise anything. return True - if not unmatched: + if not rest: to_raise = handler_excs # raise what handler returned elif not handler_excs: - to_raise = unmatched # raise the unmatched exceptions + to_raise = rest # raise the rest exceptions else: # to_keep: EG subgroup of exc with only those to reraise # (either not matched or reraised by handler) to_keep = exc.subgroup( - list(unmatched) + [e for e in handler_excs if e in match]) + list(rest) + [e for e in handler_excs if e in match]) # to_add: new exceptions raised by handler to_add = handler_excs.subgroup( [e for e in handler_excs if e not in match]) - to_raise = ExceptionGroup([to_keep, to_add]) # When we raise to_raise, Python will unconditionally blow diff --git a/Lib/test/test_exception_group.py b/Lib/test/test_exception_group.py index eccc828c22f975..dc16430615b695 100644 --- a/Lib/test/test_exception_group.py +++ b/Lib/test/test_exception_group.py @@ -301,7 +301,7 @@ def test_split_nested(self): match, rest = self._split_exception_group(eg, SyntaxError) checkMatch(eg, eg_template) - checkMatch(match, [[],[],[]]) + checkMatch(match, []) checkMatch(rest, eg_template) match, rest = self._split_exception_group(eg, ValueError) @@ -322,7 +322,7 @@ def test_split_nested(self): match, rest = self._split_exception_group(eg, (ValueError, TypeError)) checkMatch(eg, eg_template) checkMatch(match, eg_template) - checkMatch(rest, [[],[],[]]) + checkMatch(rest, []) class ExceptionGroupCatchTests(ExceptionGroupTestUtils): def checkMatch(self, exc, template, reference_tbs): @@ -502,5 +502,138 @@ def handler(eg): ref_tbs) self.checkMatch(caught, self._reduce(eg_template, ValueError), ref_tbs) + def test_catch_nested_eg_handler_reraise_all_matched(self): + def handler(eg): + return eg + + try: + self.nested_exception_group() + except ExceptionGroup as eg: + eg1 = eg + eg_template = self.to_template(eg) + + ref_tbs = {} + for e in eg1: + tb = [self.funcname(f) for f in eg1.extract_traceback(e)] + ref_tbs[(type(e), e.args)] = tb + + try: ######### Catch TypeErrors: + raised = None + with ExceptionGroup.catch(TypeError, handler): + self.nested_exception_group() + except ExceptionGroup as eg: + raised = eg + self.checkMatch(raised, eg_template, ref_tbs) + + try: ######### Catch ValueErrors: + raised = None + with ExceptionGroup.catch((ValueError, SyntaxError), handler): + self.nested_exception_group() + except ExceptionGroup as eg: + raised = eg + self.checkMatch(raised, eg_template, ref_tbs) + + def test_catch_nested_eg_handler_reraise_new_and_all_old(self): + def handler(eg): + return ExceptionGroup( + [eg, + ValueError('foo'), + ExceptionGroup( + [SyntaxError('bar'), ValueError('baz')])]) + + try: + self.nested_exception_group() + except ExceptionGroup as eg: + eg1 = eg + eg_template = self.to_template(eg) + + class DummyException(Exception): pass + try: + raise handler(DummyException()) + except ExceptionGroup as eg: + _, eg2 = eg.split(DummyException) + new_raised_template = self.to_template(eg2) + + ref_tbs = {} + for eg in (eg1, eg2): + for e in eg: + tb = [self.funcname(f) for f in eg.extract_traceback(e)] + ref_tbs[(type(e), e.args)] = tb + + try: ######### Catch TypeErrors: + raised = None + with ExceptionGroup.catch(TypeError, handler): + self.nested_exception_group() + except ExceptionGroup as eg: + raised = eg + self.checkMatch(raised, [eg_template, new_raised_template], ref_tbs) + + try: ######### Catch ValueErrors: + raised = None + with ExceptionGroup.catch((ValueError, SyntaxError), handler): + self.nested_exception_group() + except ExceptionGroup as eg: + raised = eg + self.checkMatch(raised, [eg_template, new_raised_template], ref_tbs) + + def test_catch_nested_eg_handler_reraise_new_and_some_old(self): + def handler(eg): + ret = ExceptionGroup( + [eg.excs[1], + ValueError('foo'), + ExceptionGroup( + [SyntaxError('bar'), ValueError('baz')])]) + return ret + + try: + self.nested_exception_group() + except ExceptionGroup as eg: + eg1 = eg + eg_template = self.to_template(eg) + + class DummyException(Exception): pass + try: + eg = ExceptionGroup([DummyException(), DummyException()]) + raise handler(eg) + except ExceptionGroup as eg: + _, eg2 = eg.split(DummyException) + new_raised_template = self.to_template(eg2) + + ref_tbs = {} + for eg in (eg1, eg2): + for e in eg: + tb = [self.funcname(f) for f in eg.extract_traceback(e)] + ref_tbs[(type(e), e.args)] = tb + + try: ######### Catch TypeErrors: + raised = None + with ExceptionGroup.catch(TypeError, handler): + self.nested_exception_group() + except ExceptionGroup as eg: + raised = eg + self.checkMatch(raised, + [ + [ self._reduce(eg_template[0], ValueError), + eg_template[1], + self._reduce(eg_template[2], ValueError), + ], + new_raised_template], + ref_tbs) + + try: ######### Catch ValueErrors: + raised = None + with ExceptionGroup.catch((ValueError, SyntaxError), handler): + self.nested_exception_group() + except ExceptionGroup as eg: + raised = eg + self.checkMatch(raised, + [ + [ self._reduce(eg_template[0], TypeError), + eg_template[1], + self._reduce(eg_template[2], TypeError), + ], + new_raised_template], + ref_tbs) + if __name__ == '__main__': unittest.main() From 9b671ab082a2e9f7f05911e1d7772077641ece67 Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Fri, 30 Oct 2020 16:25:14 +0000 Subject: [PATCH 28/73] revert change to Lib\types.py --- Lib/types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/types.py b/Lib/types.py index 3acf6808324e61..532f4806fc0226 100644 --- a/Lib/types.py +++ b/Lib/types.py @@ -62,7 +62,7 @@ def _m(self): pass GetSetDescriptorType = type(FunctionType.__code__) MemberDescriptorType = type(FunctionType.__globals__) -del _f, _g, _C, _c, _ag # Not for export +del sys, _f, _g, _C, _c, _ag # Not for export # Provide a PEP 3115 compliant mechanism for class creation From 618e2512796bae0789856dca5010c9ab63ea7e2b Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Fri, 30 Oct 2020 17:34:43 +0000 Subject: [PATCH 29/73] update Lib/asyncio/taskgroup.py and tg1 with new location of EG --- Lib/asyncio/taskgroup.py | 4 ++-- tg1.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Lib/asyncio/taskgroup.py b/Lib/asyncio/taskgroup.py index 55e8bc2c67e0f6..d62e9c36c3210d 100644 --- a/Lib/asyncio/taskgroup.py +++ b/Lib/asyncio/taskgroup.py @@ -23,7 +23,7 @@ import functools import itertools import sys -import types +import exception_group __all__ = ('TaskGroup',) @@ -165,7 +165,7 @@ async def __aexit__(self, et, exc, tb): errors = self._errors self._errors = None - raise types.ExceptionGroup(errors) + raise exception_group.ExceptionGroup(errors) def create_task(self, coro): if not self._entered: diff --git a/tg1.py b/tg1.py index 6bb00e75650847..2e60eab62d7007 100644 --- a/tg1.py +++ b/tg1.py @@ -1,5 +1,5 @@ import asyncio -import types +import exception_group async def t1(): await asyncio.sleep(0.5) @@ -27,9 +27,9 @@ async def main(): def run(*args): try: asyncio.run(*args) - except types.ExceptionGroup as e: + except exception_group.ExceptionGroup as e: print('============') - types.ExceptionGroup.render(e) + exception_group.ExceptionGroup.render(e) print('^^^^^^^^^^^^') raise From 791ff8d229b18154486bd1e544af47f62f034d0b Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Fri, 30 Oct 2020 19:37:58 +0000 Subject: [PATCH 30/73] _split_on_condition --> project --- Lib/exception_group.py | 50 ++++++++++++++++++++++++++++-------------- 1 file changed, 33 insertions(+), 17 deletions(-) diff --git a/Lib/exception_group.py b/Lib/exception_group.py index 3c931feb806d39..39af9cac5391f1 100644 --- a/Lib/exception_group.py +++ b/Lib/exception_group.py @@ -7,6 +7,7 @@ def __init__(self, excs): # TODO: Oy, this needs to be a weak key dict, but exceptions # are not weakreffable. # TODO: what if e is unhashable? + # TODO: Why don't we make this a list corresponding to excs? self.tb_next_map = {} for e in excs: if isinstance(e, ExceptionGroup): @@ -21,6 +22,12 @@ def __init__(self, excs): class ExceptionGroup(BaseException): def __init__(self, excs, *, tb=None): + """ Construct a new ExceptionGroup + + excs: sequence of exceptions + tb [optional]: the __traceback__ of this exception group. + Typically set when this ExceptionGroup is derived from another. + """ self.excs = excs # self.__traceback__ is updated as usual, but self.__traceback_group__ # is set when the exception group is created. @@ -28,11 +35,20 @@ def __init__(self, excs, *, tb=None): self.__traceback__ = tb self.__traceback_group__ = TracebackGroup(self.excs) - def _split_on_condition(self, condition): + def project(self, condition): + """ Split an ExceptionGroup based on an exception predicate + + returns two new ExceptionGroups: match, rest of the exceptions + of self for which condition(e) returns True and False, respectively. + match and rest have the same nested structure as self, but empty + sub-exceptions are not included. + + condition: BaseException --> Boolean + """ match, rest = [], [] for e in self.excs: if isinstance(e, ExceptionGroup): # recurse - e_match, e_rest = e._split_on_condition(condition) + e_match, e_rest = e.project(condition) if e_match: match.append(e_match) if e_rest: @@ -46,23 +62,19 @@ def _split_on_condition(self, condition): return (ExceptionGroup(match, tb=self.__traceback__), ExceptionGroup(rest, tb=self.__traceback__)) - def split(self, E): - """ Split an ExceptionGroup to extract exceptions matching E + def split(self, type): + """ Split an ExceptionGroup to extract exceptions of type E - returns two new ExceptionGroups: match, rest of the exceptions of - self that match E and those that don't. - match and rest have the same nested structure as self. - E can be a type or tuple of types. + type: An exception type """ - return self._split_on_condition(lambda e: isinstance(e, E)) + return self.project(lambda e: isinstance(e, type)) def subgroup(self, keep): - """ Return a subgroup of self including only the exception in keep + """ Split an ExceptionGroup to extract only exceptions in keep - returns a new ExceptionGroups that contains only the exception in - the sequence keep and preserves the internal structure of self. + keep: List[BaseException] """ - match, _ = self._split_on_condition(lambda e: e in keep) + match, _ = self.project(lambda e: e in keep) return match def push_frame(self, frame): @@ -103,18 +115,22 @@ def extract_traceback(self, exc): @staticmethod def render(exc, tb=None, indent=0): - print(exc) + output = [] + output.append(f"{exc}") tb = tb or exc.__traceback__ while tb and not isinstance(tb, TracebackGroup): - print(' '*indent, tb.tb_frame) + output.append(f"{' '*indent} {tb.tb_frame}") tb = tb.tb_next if isinstance(exc, ExceptionGroup): tbg = exc.__traceback_group__ assert isinstance(tbg, TracebackGroup) indent += 4 for e, t in tbg.tb_next_map.items(): - print('---------------------------------------') - ExceptionGroup.render(e, t, indent) + output.append('---------------------------------------') + output.extend(ExceptionGroup.render(e, t, indent)) + for l in output: + print(l) + return output def __iter__(self): ''' iterate over the individual exceptions (flattens the tree) ''' From f6c1442d181a8ab4cf81c9e1cf17fec25a9883af Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Sat, 31 Oct 2020 15:37:03 +0000 Subject: [PATCH 31/73] use subgroup to simplify extract_traceback --- Lib/exception_group.py | 36 ++++++++++++++---------------------- 1 file changed, 14 insertions(+), 22 deletions(-) diff --git a/Lib/exception_group.py b/Lib/exception_group.py index 39af9cac5391f1..ff972768c4280d 100644 --- a/Lib/exception_group.py +++ b/Lib/exception_group.py @@ -77,42 +77,34 @@ def subgroup(self, keep): match, _ = self.project(lambda e: e in keep) return match - def push_frame(self, frame): - import types - self.__traceback__ = types.TracebackType( - self.__traceback__, frame, 0, 0) - def extract_traceback(self, exc): """ returns the traceback of a single exception If exc is in this exception group, return its traceback as a list of frames. Otherwise, return None. - - Note: The frame where an exception was caught and - rereaised as part of an exception group appreas twice. """ if exc not in self: return None result = [] - tb = self.__traceback__ - while tb: - result.append(tb.tb_frame) - tb = tb.tb_next - next_e = None - for e in self.excs: - if exc == e or (isinstance(e, ExceptionGroup) and exc in e): - assert next_e is None - next_e = e - assert next_e is not None - if isinstance(next_e, ExceptionGroup): - result.extend(next_e.extract_traceback(exc)) - else: - tb = next_e.__traceback__ + e = self.subgroup([exc]) + while e: + tb = e.__traceback__ while tb: result.append(tb.tb_frame) tb = tb.tb_next + if isinstance(e, ExceptionGroup): + assert len(e.excs) == 1 and exc in e + e = e.excs[0] + else: + assert e is exc + e = None return result + def push_frame(self, frame): + import types + self.__traceback__ = types.TracebackType( + self.__traceback__, frame, 0, 0) + @staticmethod def render(exc, tb=None, indent=0): output = [] From c78848bf357cc22c51e707e3e30229ebd2990dab Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Sat, 31 Oct 2020 18:12:04 +0000 Subject: [PATCH 32/73] don't create a new EG if to_add is empty --- Lib/exception_group.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Lib/exception_group.py b/Lib/exception_group.py index ff972768c4280d..ecdfec0d4e8024 100644 --- a/Lib/exception_group.py +++ b/Lib/exception_group.py @@ -199,7 +199,10 @@ def __exit__(self, etype, exc, tb): # to_add: new exceptions raised by handler to_add = handler_excs.subgroup( [e for e in handler_excs if e not in match]) - to_raise = ExceptionGroup([to_keep, to_add]) + if to_add: + to_raise = ExceptionGroup([to_keep, to_add]) + else: + to_raise = to_keep # When we raise to_raise, Python will unconditionally blow # away its __context__ attribute and replace it with the original From 1ec4bce3b07ce739f23a2ca016142de5f804635f Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Sat, 31 Oct 2020 18:19:19 +0000 Subject: [PATCH 33/73] tb_next_map keyed on id(exc). Do we need anything else? (array of tbs? weak refs?) --- Lib/exception_group.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/Lib/exception_group.py b/Lib/exception_group.py index ecdfec0d4e8024..2e22faa6a61b86 100644 --- a/Lib/exception_group.py +++ b/Lib/exception_group.py @@ -4,20 +4,16 @@ class TracebackGroup: def __init__(self, excs): - # TODO: Oy, this needs to be a weak key dict, but exceptions - # are not weakreffable. - # TODO: what if e is unhashable? - # TODO: Why don't we make this a list corresponding to excs? - self.tb_next_map = {} + self.tb_next_map = {} # exception id to tb for e in excs: if isinstance(e, ExceptionGroup): for e_ in e.excs: - self.tb_next_map[e_] = e_.__traceback__ + self.tb_next_map[id(e_)] = e_.__traceback__ else: if e.__traceback__: - self.tb_next_map[e] = e.__traceback__.tb_next + self.tb_next_map[id(e)] = e.__traceback__.tb_next else: - self.tb_next_map[e] = None + self.tb_next_map[id(e)] = None class ExceptionGroup(BaseException): @@ -117,7 +113,8 @@ def render(exc, tb=None, indent=0): tbg = exc.__traceback_group__ assert isinstance(tbg, TracebackGroup) indent += 4 - for e, t in tbg.tb_next_map.items(): + for e in exc.excs: + t = tbg.tb_next_map[id(e)] output.append('---------------------------------------') output.extend(ExceptionGroup.render(e, t, indent)) for l in output: From 4d63a8ae7cb87786176a8aae9e68427e4f26d9c1 Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Sun, 1 Nov 2020 15:48:26 +0000 Subject: [PATCH 34/73] Guido's review comments (easy ones done, complex ones noted as TODO) --- Lib/asyncio/__init__.py | 4 +- Lib/asyncio/taskgroup.py | 282 ------------------------------- Lib/exception_group.py | 30 ++-- Lib/test/test_exception_group.py | 4 +- 4 files changed, 21 insertions(+), 299 deletions(-) delete mode 100644 Lib/asyncio/taskgroup.py diff --git a/Lib/asyncio/__init__.py b/Lib/asyncio/__init__.py index 7501179d9be75d..eb84bfb189ccf3 100644 --- a/Lib/asyncio/__init__.py +++ b/Lib/asyncio/__init__.py @@ -19,7 +19,6 @@ from .tasks import * from .threads import * from .transports import * -from .taskgroup import * # Exposed for _asynciomodule.c to implement now deprecated # Task.all_tasks() method. This function will be removed in 3.9. @@ -38,8 +37,7 @@ subprocess.__all__ + tasks.__all__ + threads.__all__ + - transports.__all__ + - taskgroup.__all__) + transports.__all__) if sys.platform == 'win32': # pragma: no cover from .windows_events import * diff --git a/Lib/asyncio/taskgroup.py b/Lib/asyncio/taskgroup.py deleted file mode 100644 index d62e9c36c3210d..00000000000000 --- a/Lib/asyncio/taskgroup.py +++ /dev/null @@ -1,282 +0,0 @@ -# -# This source file is part of the EdgeDB open source project. -# -# Copyright 2016-present MagicStack Inc. and the EdgeDB authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - - -from __future__ import annotations - -import asyncio -import functools -import itertools -import sys -import exception_group - -__all__ = ('TaskGroup',) - - -class TaskGroup: - - def __init__(self, *, name=None): - if name is None: - self._name = f'tg-{_name_counter()}' - else: - self._name = str(name) - - self._entered = False - self._exiting = False - self._aborting = False - self._loop = None - self._parent_task = None - self._parent_cancel_requested = False - self._tasks = set() - self._unfinished_tasks = 0 - self._errors = [] - self._base_error = None - self._on_completed_fut = None - - def get_name(self): - return self._name - - def __repr__(self): - msg = f'= (3, 8): - - # In Python 3.8 Tasks propagate all exceptions correctly, - # except for KeyboardInterrupt and SystemExit which are - # still considered special. - - def _is_base_error(self, exc: BaseException) -> bool: - assert isinstance(exc, BaseException) - return isinstance(exc, (SystemExit, KeyboardInterrupt)) - - else: - - # In Python prior to 3.8 all BaseExceptions are special and - # are bypassing the proper propagation through async/await - # code, essentially aborting the execution. - - def _is_base_error(self, exc: BaseException) -> bool: - assert isinstance(exc, BaseException) - return not isinstance(exc, Exception) - - def _patch_task(self, task): - # In Python 3.8 we'll need proper API on asyncio.Task to - # make TaskGroups possible. We need to be able to access - # information about task cancellation, more specifically, - # we need a flag to say if a task was cancelled or not. - # We also need to be able to flip that flag. - - def _task_cancel(task, orig_cancel): - task.__cancel_requested__ = True - return orig_cancel() - - if hasattr(task, '__cancel_requested__'): - return - - task.__cancel_requested__ = False - # confirm that we were successful at adding the new attribute: - assert not task.__cancel_requested__ - - orig_cancel = task.cancel - task.cancel = functools.partial(_task_cancel, task, orig_cancel) - - def _abort(self): - self._aborting = True - - for t in self._tasks: - if not t.done(): - t.cancel() - - def _on_task_done(self, task): - self._unfinished_tasks -= 1 - assert self._unfinished_tasks >= 0 - - if self._exiting and not self._unfinished_tasks: - if not self._on_completed_fut.done(): - self._on_completed_fut.set_result(True) - - if task.cancelled(): - return - - exc = task.exception() - if exc is None: - return - - self._errors.append(exc) - if self._is_base_error(exc) and self._base_error is None: - self._base_error = exc - - if self._parent_task.done(): - # Not sure if this case is possible, but we want to handle - # it anyways. - self._loop.call_exception_handler({ - 'message': f'Task {task!r} has errored out but its parent ' - f'task {self._parent_task} is already completed', - 'exception': exc, - 'task': task, - }) - return - - self._abort() - if not self._parent_task.__cancel_requested__: - # If parent task *is not* being cancelled, it means that we want - # to manually cancel it to abort whatever is being run right now - # in the TaskGroup. But we want to mark parent task as - # "not cancelled" later in __aexit__. Example situation that - # we need to handle: - # - # async def foo(): - # try: - # async with TaskGroup() as g: - # g.create_task(crash_soon()) - # await something # <- this needs to be canceled - # # by the TaskGroup, e.g. - # # foo() needs to be cancelled - # except Exception: - # # Ignore any exceptions raised in the TaskGroup - # pass - # await something_else # this line has to be called - # # after TaskGroup is finished. - self._parent_cancel_requested = True - self._parent_task.cancel() - -_name_counter = itertools.count(1).__next__ diff --git a/Lib/exception_group.py b/Lib/exception_group.py index 2e22faa6a61b86..4e5350ac4ab597 100644 --- a/Lib/exception_group.py +++ b/Lib/exception_group.py @@ -8,16 +8,18 @@ def __init__(self, excs): for e in excs: if isinstance(e, ExceptionGroup): for e_ in e.excs: + # TODO: what if e_ is an EG? This looks wrong. self.tb_next_map[id(e_)] = e_.__traceback__ else: if e.__traceback__: + # TODO: is tb_next always correct? explain why. self.tb_next_map[id(e)] = e.__traceback__.tb_next else: self.tb_next_map[id(e)] = None class ExceptionGroup(BaseException): - def __init__(self, excs, *, tb=None): + def __init__(self, excs, *, msg=None, tb=None): """ Construct a new ExceptionGroup excs: sequence of exceptions @@ -25,6 +27,7 @@ def __init__(self, excs, *, tb=None): Typically set when this ExceptionGroup is derived from another. """ self.excs = excs + self.msg = msg # self.__traceback__ is updated as usual, but self.__traceback_group__ # is set when the exception group is created. # __traceback_group__ and __traceback__ combine to give the full path. @@ -41,6 +44,7 @@ def project(self, condition): condition: BaseException --> Boolean """ + # TODO: add option to not create 'rest' match, rest = [], [] for e in self.excs: if isinstance(e, ExceptionGroup): # recurse @@ -52,11 +56,14 @@ def project(self, condition): else: if condition(e): match.append(e) - e_match, e_rest = e, None else: rest.append(e) - return (ExceptionGroup(match, tb=self.__traceback__), - ExceptionGroup(rest, tb=self.__traceback__)) + match_exc = ExceptionGroup(match, tb=self.__traceback__) + rest_exc = ExceptionGroup(rest, tb=self.__traceback__) + match_exc.msg = rest_exc.msg = self.msg + match_exc.__cause__ = rest_exc.__cause__ = self.__cause__ + match_exc.__context__ = rest_exc.__context__ = self.__context__ + return match_exc, rest_exc def split(self, type): """ Split an ExceptionGroup to extract exceptions of type E @@ -79,13 +86,15 @@ def extract_traceback(self, exc): If exc is in this exception group, return its traceback as a list of frames. Otherwise, return None. """ + # TODO: integrate into traceback.py style + # TODO: return a traceback.StackSummary ? if exc not in self: return None result = [] e = self.subgroup([exc]) while e: tb = e.__traceback__ - while tb: + while tb is not None: result.append(tb.tb_frame) tb = tb.tb_next if isinstance(e, ExceptionGroup): @@ -96,17 +105,13 @@ def extract_traceback(self, exc): e = None return result - def push_frame(self, frame): - import types - self.__traceback__ = types.TracebackType( - self.__traceback__, frame, 0, 0) - @staticmethod def render(exc, tb=None, indent=0): + # TODO: integrate into traceback.py style output = [] output.append(f"{exc}") tb = tb or exc.__traceback__ - while tb and not isinstance(tb, TracebackGroup): + while tb is not None and not isinstance(tb, TracebackGroup): output.append(f"{' '*indent} {tb.tb_frame}") tb = tb.tb_next if isinstance(exc, ExceptionGroup): @@ -130,6 +135,7 @@ def __iter__(self): else: yield e + # TODO: replace len by is_empty() def __len__(self): l = 0 for e in self.excs: @@ -174,7 +180,7 @@ def __exit__(self, etype, exc, tb): return False handler_excs = self.handler(match) - if handler_excs == match: + if handler_excs is match: # handler reraised all of the matched exceptions. # reraise exc as is. return False diff --git a/Lib/test/test_exception_group.py b/Lib/test/test_exception_group.py index dc16430615b695..af9232b8d22f53 100644 --- a/Lib/test/test_exception_group.py +++ b/Lib/test/test_exception_group.py @@ -226,8 +226,8 @@ def _split_exception_group(self, eg, types): self.assertIsInstance(match, ExceptionGroup) self.assertIsInstance(rest, ExceptionGroup) - self.assertEqual(len(all_excs), len(eg)) - self.assertEqual(len(all_excs), len(match) + len(rest)) + self.assertEqual(len(list(all_excs)), len(list(eg))) + self.assertEqual(len(list(all_excs)), len(list(match)) + len(list(rest))) for e in all_excs: self.assertIn(e, eg) # every exception in all_excs is in eg and From 040ebcfaf0bc827988a66635de0b46720721d103 Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Sun, 1 Nov 2020 16:30:28 +0000 Subject: [PATCH 35/73] replace __len__ by is_empty --- Lib/exception_group.py | 42 ++++++++++++++++++++---------------------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/Lib/exception_group.py b/Lib/exception_group.py index 4e5350ac4ab597..fed52017d71118 100644 --- a/Lib/exception_group.py +++ b/Lib/exception_group.py @@ -49,9 +49,9 @@ def project(self, condition): for e in self.excs: if isinstance(e, ExceptionGroup): # recurse e_match, e_rest = e.project(condition) - if e_match: + if not e_match.is_empty(): match.append(e_match) - if e_rest: + if not e_rest.is_empty(): rest.append(e_rest) else: if condition(e): @@ -92,7 +92,9 @@ def extract_traceback(self, exc): return None result = [] e = self.subgroup([exc]) - while e: + while e is not None and\ + (not isinstance(e, ExceptionGroup) or not e.is_empty()): + tb = e.__traceback__ while tb is not None: result.append(tb.tb_frame) @@ -135,15 +137,8 @@ def __iter__(self): else: yield e - # TODO: replace len by is_empty() - def __len__(self): - l = 0 - for e in self.excs: - if isinstance(e, ExceptionGroup): - l += len(e) - else: - l += 1 - return l + def is_empty(self): + return not any(self) def __repr__(self): return f"ExceptionGroup({self.excs})" @@ -175,7 +170,7 @@ def __exit__(self, etype, exc, tb): if exc is not None and isinstance(exc, ExceptionGroup): match, rest = exc.split(self.types) - if not match: + if match.is_empty(): # Let the interpreter reraise the exception return False @@ -185,16 +180,18 @@ def __exit__(self, etype, exc, tb): # reraise exc as is. return False - if not handler_excs and not rest: - # handled and swallowed all exceptions - # do not raise anything. - return True - - if not rest: + if handler_excs is None or handler_excs.is_empty(): + if rest.is_empty(): + # handled and swallowed all exceptions + # do not raise anything. + return True + else: + # raise the rest exceptions + to_raise = rest + elif rest.is_empty(): to_raise = handler_excs # raise what handler returned - elif not handler_excs: - to_raise = rest # raise the rest exceptions else: + # Merge handler's exceptions with rest # to_keep: EG subgroup of exc with only those to reraise # (either not matched or reraised by handler) to_keep = exc.subgroup( @@ -202,8 +199,9 @@ def __exit__(self, etype, exc, tb): # to_add: new exceptions raised by handler to_add = handler_excs.subgroup( [e for e in handler_excs if e not in match]) - if to_add: + if not to_add.is_empty(): to_raise = ExceptionGroup([to_keep, to_add]) + to_raise.msg = exc.msg else: to_raise = to_keep From 286f4ad5c73338ad21685c83a912fccfab6529b6 Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Sun, 1 Nov 2020 16:53:12 +0000 Subject: [PATCH 36/73] make complement EG optional in project() --- Lib/exception_group.py | 42 ++++++++++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/Lib/exception_group.py b/Lib/exception_group.py index fed52017d71118..15c1d31c6f89da 100644 --- a/Lib/exception_group.py +++ b/Lib/exception_group.py @@ -34,35 +34,47 @@ def __init__(self, excs, *, msg=None, tb=None): self.__traceback__ = tb self.__traceback_group__ = TracebackGroup(self.excs) - def project(self, condition): + def project(self, condition, with_complement=False): """ Split an ExceptionGroup based on an exception predicate - returns two new ExceptionGroups: match, rest of the exceptions - of self for which condition(e) returns True and False, respectively. + returns a new ExceptionGroup, match, of the exceptions of self + for which condition returns True. If with_complement is True, + returns another ExceptionGroup for the exception for which + condition returns False. match and rest have the same nested structure as self, but empty - sub-exceptions are not included. + sub-exceptions are not included. They have the same msg, + __traceback__, __cause__ and __context__ fields as self. condition: BaseException --> Boolean + with_complement: Bool If True, construct also an EG of the non-matches """ - # TODO: add option to not create 'rest' - match, rest = [], [] + match = [] + rest = [] if with_complement else None for e in self.excs: if isinstance(e, ExceptionGroup): # recurse - e_match, e_rest = e.project(condition) + e_match, e_rest = e.project( + condition, with_complement=with_complement) if not e_match.is_empty(): match.append(e_match) - if not e_rest.is_empty(): + if with_complement and not e_rest.is_empty(): rest.append(e_rest) else: if condition(e): match.append(e) - else: + elif with_complement: rest.append(e) + match_exc = ExceptionGroup(match, tb=self.__traceback__) - rest_exc = ExceptionGroup(rest, tb=self.__traceback__) - match_exc.msg = rest_exc.msg = self.msg - match_exc.__cause__ = rest_exc.__cause__ = self.__cause__ - match_exc.__context__ = rest_exc.__context__ = self.__context__ + def copy_metadata(src, target): + target.msg = src.msg + target.__context__ = src.__context__ + target.__cause__ = src.__cause__ + copy_metadata(self, match_exc) + if with_complement: + rest_exc = ExceptionGroup(rest, tb=self.__traceback__) + copy_metadata(self, rest_exc) + else: + rest_exc = None return match_exc, rest_exc def split(self, type): @@ -70,7 +82,9 @@ def split(self, type): type: An exception type """ - return self.project(lambda e: isinstance(e, type)) + return self.project( + lambda e: isinstance(e, type), + with_complement=True) def subgroup(self, keep): """ Split an ExceptionGroup to extract only exceptions in keep From 742f8fa73778b071817276a263e64f3099d7f153 Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Mon, 2 Nov 2020 00:49:04 +0000 Subject: [PATCH 37/73] fix TracebackGroup constructor for nested EGs --- Lib/exception_group.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Lib/exception_group.py b/Lib/exception_group.py index 15c1d31c6f89da..980cd9a5215e09 100644 --- a/Lib/exception_group.py +++ b/Lib/exception_group.py @@ -8,8 +8,11 @@ def __init__(self, excs): for e in excs: if isinstance(e, ExceptionGroup): for e_ in e.excs: - # TODO: what if e_ is an EG? This looks wrong. - self.tb_next_map[id(e_)] = e_.__traceback__ + if isinstance(e_, ExceptionGroup): + for k in e_: + self.tb_next_map[id(k)] = e_.__traceback__ + else: + self.tb_next_map[id(e_)] = e_.__traceback__ else: if e.__traceback__: # TODO: is tb_next always correct? explain why. @@ -20,6 +23,7 @@ def __init__(self, excs): class ExceptionGroup(BaseException): def __init__(self, excs, *, msg=None, tb=None): + # TODO: msg arg comes first """ Construct a new ExceptionGroup excs: sequence of exceptions From e21b368d232521ccf4d3670b46ef6070fec8c5c1 Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Mon, 2 Nov 2020 13:56:34 +0000 Subject: [PATCH 38/73] rewrote construction tests, fixed TraceBackGroup init --- Lib/exception_group.py | 15 ++- Lib/test/test_exception_group.py | 172 ++++++++++++++++--------------- 2 files changed, 95 insertions(+), 92 deletions(-) diff --git a/Lib/exception_group.py b/Lib/exception_group.py index 980cd9a5215e09..376dadb7253e79 100644 --- a/Lib/exception_group.py +++ b/Lib/exception_group.py @@ -9,16 +9,13 @@ def __init__(self, excs): if isinstance(e, ExceptionGroup): for e_ in e.excs: if isinstance(e_, ExceptionGroup): - for k in e_: - self.tb_next_map[id(k)] = e_.__traceback__ + ks = list(e_) else: - self.tb_next_map[id(e_)] = e_.__traceback__ + ks = (e_,) + for k in ks: + self.tb_next_map[id(k)] = e_.__traceback__ else: - if e.__traceback__: - # TODO: is tb_next always correct? explain why. - self.tb_next_map[id(e)] = e.__traceback__.tb_next - else: - self.tb_next_map[id(e)] = None + self.tb_next_map[id(e)] = e.__traceback__ class ExceptionGroup(BaseException): @@ -129,7 +126,7 @@ def extract_traceback(self, exc): def render(exc, tb=None, indent=0): # TODO: integrate into traceback.py style output = [] - output.append(f"{exc}") + output.append(f"{exc!r}") tb = tb or exc.__traceback__ while tb is not None and not isinstance(tb, TracebackGroup): output.append(f"{' '*indent} {tb.tb_frame}") diff --git a/Lib/test/test_exception_group.py b/Lib/test/test_exception_group.py index af9232b8d22f53..82c3fa5ea635eb 100644 --- a/Lib/test/test_exception_group.py +++ b/Lib/test/test_exception_group.py @@ -1,4 +1,5 @@ +import functools import unittest import collections.abc from exception_group import ExceptionGroup, TracebackGroup @@ -18,6 +19,19 @@ def assertMatchesTemplate(self, exc, template): self.assertEqual(type(exc), type(template)) self.assertEqual(exc.args, template.args) + def tracebackGroupSanityCheck(self, exc): + if not isinstance(exc, ExceptionGroup): + return + + tbg = exc.__traceback_group__ + all_excs = list(exc) + self.assertEqual(len(tbg.tb_next_map), len(all_excs)) + self.assertEqual([i for i in tbg.tb_next_map], + [id(e) for e in exc]) + + for e in exc.excs: + self.tracebackGroupSanityCheck(e) + def to_template(self, exc): if isinstance(exc, ExceptionGroup): return [self.to_template(e) for e in exc.excs] @@ -25,6 +39,19 @@ def to_template(self, exc): return exc class ExceptionGroupTestUtils(ExceptionGroupTestBase): + + def create_EG(self, raisers): + excs = [] + for r in raisers: + try: + r() + except (Exception, ExceptionGroup) as e: + excs.append(e) + try: + raise ExceptionGroup(excs) + except ExceptionGroup as e: + return e + def raiseValueError(self, v): raise ValueError(v) @@ -84,50 +111,6 @@ def _reduce(self, template, types): return None class ExceptionGroupTestUtilsTests(ExceptionGroupTestUtils): - def test_basic_utility_functions(self): - self.assertRaises(ValueError, self.raiseValueError, 42) - self.assertRaises(TypeError, self.raiseTypeError, float) - self.assertRaises(ExceptionGroup, self.simple_exception_group, 42) - self.assertRaises(ExceptionGroup, self.nested_exception_group) - - try: - self.simple_exception_group(42) - except ExceptionGroup as eg: - template = self.to_template(eg) - self.assertEqual(len(template), 5) - expected = [ValueError(43), - TypeError('int'), - ValueError(44), - ValueError(45), - TypeError('list'), - ] - self.assertEqual(str(expected), str(template)) - - try: - self.nested_exception_group() - except ExceptionGroup as eg: - template = self.to_template(eg) - self.assertEqual(len(template), 3) - expected = [[ValueError(2), - TypeError('int'), - ValueError(3), - ValueError(4), - TypeError('list'), - ], - [ValueError(3), - TypeError('int'), - ValueError(4), - ValueError(5), - TypeError('list'), - ], - [ValueError(4), - TypeError('int'), - ValueError(5), - ValueError(6), - TypeError('list'), - ]] - self.assertEqual(str(expected), str(template)) - def test_reduce(self): te = TypeError('int') se = SyntaxError('blah') @@ -149,14 +132,16 @@ class ExceptionGroupConstructionTests(ExceptionGroupTestUtils): def test_construction_simple(self): # create a simple exception group and check that # it is constructed as expected - try: - eg = None - self.simple_exception_group(0) - except ExceptionGroup as e: - eg = e - # check eg.excs - self.assertIsInstance(eg.excs, collections.abc.Sequence) - self.assertMatchesTemplate(eg, self.to_template(eg)) + bind = functools.partial + eg = self.create_EG( + [bind(self.raiseValueError, 1), + bind(self.raiseTypeError, int), + bind(self.raiseValueError, 2), + ]) + + self.assertEqual(len(eg.excs), 3) + self.assertMatchesTemplate(eg, + [ValueError(1), TypeError(int), ValueError(2)]) # check iteration self.assertEqual(list(eg), list(eg.excs)) @@ -164,9 +149,8 @@ def test_construction_simple(self): # check tracebacks for e in eg: expected = [ - 'test_construction_simple', - 'simple_exception_group', - 'simple_exception_group', + 'create_EG', + 'create_EG', 'raise'+type(e).__name__, ] etb = eg.extract_traceback(e) @@ -175,43 +159,65 @@ def test_construction_simple(self): def test_construction_nested(self): # create a nested exception group and check that # it is constructed as expected - try: - eg = None - self.nested_exception_group() - except ExceptionGroup as e: - eg = e - # check eg.excs - self.assertIsInstance(eg.excs, collections.abc.Sequence) - self.assertEqual(len(eg.excs), 3) - - # each of eg.excs is an EG with 3xValueError and 2xTypeErrors - all_excs = [] - for e in eg.excs: - self.assertIsInstance(e, ExceptionGroup) - self.assertEqual(len(e.excs), 5) - etypes = [type(e) for e in e.excs] - self.assertEqual(etypes.count(ValueError), 3) - self.assertEqual(etypes.count(TypeError), 2) - all_excs.extend(e.excs) - - eg_template = self.to_template(eg) - self.assertMatchesTemplate(eg, eg_template) + bind = functools.partial + level1 = lambda i: self.create_EG([ + bind(self.raiseValueError, i), + bind(self.raiseTypeError, int), + bind(self.raiseValueError, i+1), + ]) + + def raiseException(e): raise e + level2 = lambda i : self.create_EG([ + bind(raiseException, level1(i)), + bind(raiseException, level1(i+1)), + bind(self.raiseValueError, i+2), + ]) + + level3 = lambda i : self.create_EG([ + bind(raiseException, level2(i+1)), + bind(self.raiseValueError, i+2), + ]) + eg = level3(5) + + self.assertMatchesTemplate(eg, + [ + [ + [ValueError(6), TypeError(int), ValueError(7)], + [ValueError(7), TypeError(int), ValueError(8)], + ValueError(8), + ], + ValueError(7) + ]) # check iteration - self.assertEqual(list(eg), all_excs) + + self.assertEqual(len(list(eg)), 8) # check tracebacks - for e in eg: + + self.tracebackGroupSanityCheck(eg) + + all_excs = list(eg) + for e in all_excs[0:6]: expected = [ - 'test_construction_nested', - 'nested_exception_group', - 'nested_exception_group', - 'simple_exception_group', - 'simple_exception_group', + 'create_EG', + 'create_EG', + 'raiseException', + 'create_EG', + 'create_EG', + 'raiseException', + 'create_EG', + 'create_EG', 'raise'+type(e).__name__, ] etb = eg.extract_traceback(e) self.assertEqual(expected, [self.funcname(f) for f in etb]) + self.assertEqual(['create_EG', 'create_EG', 'raiseException', + 'create_EG', 'create_EG', 'raiseValueError'], + [self.funcname(f) for f in eg.extract_traceback(all_excs[6])]) + self.assertEqual(['create_EG', 'create_EG', 'raiseValueError'], + [self.funcname(f) for f in eg.extract_traceback(all_excs[7])]) + class ExceptionGroupSplitTests(ExceptionGroupTestUtils): From 91af84d79863cd4fc7f125cb69b50537bd9387d2 Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Mon, 2 Nov 2020 16:02:21 +0000 Subject: [PATCH 39/73] rewrote split tests --- Lib/test/test_exception_group.py | 136 ++++++++++++++++--------------- 1 file changed, 70 insertions(+), 66 deletions(-) diff --git a/Lib/test/test_exception_group.py b/Lib/test/test_exception_group.py index 82c3fa5ea635eb..9f26de63c034f2 100644 --- a/Lib/test/test_exception_group.py +++ b/Lib/test/test_exception_group.py @@ -231,9 +231,8 @@ def _split_exception_group(self, eg, types): self.assertIsInstance(match, ExceptionGroup) self.assertIsInstance(rest, ExceptionGroup) - - self.assertEqual(len(list(all_excs)), len(list(eg))) self.assertEqual(len(list(all_excs)), len(list(match)) + len(list(rest))) + for e in all_excs: self.assertIn(e, eg) # every exception in all_excs is in eg and @@ -254,81 +253,86 @@ def _split_exception_group(self, eg, types): return match, rest - def test_split_simple(self): - checkMatch = self.assertMatchesTemplate - try: - eg = None - self.simple_exception_group(5) - except ExceptionGroup as e: - eg = e - fnames = ['test_split_simple', 'simple_exception_group'] - self.assertEqual(self.funcnames(eg.__traceback__), fnames) - - eg_template = self.to_template(eg) - checkMatch(eg, eg_template) - - match, rest = self._split_exception_group(eg, SyntaxError) - checkMatch(eg, eg_template) - checkMatch(match, []) - checkMatch(rest, eg_template) - - match, rest = self._split_exception_group(eg, ValueError) - checkMatch(eg, eg_template) - checkMatch(match, self._reduce(eg_template, ValueError)) - checkMatch(rest, self._reduce(eg_template, TypeError)) - - match, rest = self._split_exception_group(eg, TypeError) - checkMatch(eg, eg_template) - checkMatch(match, self._reduce(eg_template, TypeError)) - checkMatch(rest, self._reduce(eg_template, ValueError)) - - match, rest = self._split_exception_group(eg, (ValueError, SyntaxError)) - checkMatch(eg, eg_template) - checkMatch(match, self._reduce(eg_template, ValueError)) - checkMatch(rest, self._reduce(eg_template, TypeError)) + def test_split_nested(self): + # create a nested exception group and check that + # it is constructed as expected + bind = functools.partial + level1 = lambda i: self.create_EG([ + bind(self.raiseValueError, i), + bind(self.raiseTypeError, int), + bind(self.raiseValueError, i+10), + ]) - match, rest = self._split_exception_group(eg, (ValueError, TypeError)) - checkMatch(eg, eg_template) - checkMatch(match, eg_template) - checkMatch(rest, []) + def raiseException(e): raise e + level2 = lambda i : self.create_EG([ + bind(raiseException, level1(i)), + bind(raiseException, level1(i+20)), + bind(self.raiseValueError, i+30), + ]) - def test_split_nested(self): - checkMatch = self.assertMatchesTemplate + level3 = lambda i : self.create_EG([ + bind(raiseException, level2(i+40)), + bind(self.raiseValueError, i+50), + ]) try: - eg = None - self.nested_exception_group() + raise level3(5) except ExceptionGroup as e: eg = e - fnames = ['test_split_nested', 'nested_exception_group'] - self.assertEqual(self.funcnames(eg.__traceback__), fnames) - eg_template = self.to_template(eg) - checkMatch(eg, eg_template) + fnames = ['test_split_nested', 'create_EG'] + self.assertEqual(self.funcnames(eg.__traceback__), fnames) + eg_template = [ + [ + [ValueError(45), TypeError(int), ValueError(55)], + [ValueError(65), TypeError(int), ValueError(75)], + ValueError(75), + ], + ValueError(55) + ] + self.assertMatchesTemplate(eg, eg_template) + + # Match Nothing match, rest = self._split_exception_group(eg, SyntaxError) - checkMatch(eg, eg_template) - checkMatch(match, []) - checkMatch(rest, eg_template) - - match, rest = self._split_exception_group(eg, ValueError) - checkMatch(eg, eg_template) - checkMatch(match, self._reduce(eg_template, ValueError)) - checkMatch(rest, self._reduce(eg_template, TypeError)) + self.assertTrue(match.is_empty()) + self.assertMatchesTemplate(rest, eg_template) - match, rest = self._split_exception_group(eg, TypeError) - checkMatch(eg, eg_template) - checkMatch(match, self._reduce(eg_template, TypeError)) - checkMatch(rest, self._reduce(eg_template, ValueError)) + # Match Everything + match, rest = self._split_exception_group(eg, BaseException) + self.assertMatchesTemplate(match, eg_template) + self.assertTrue(rest.is_empty()) + match, rest = self._split_exception_group(eg, (ValueError, TypeError)) + self.assertMatchesTemplate(match, eg_template) + self.assertTrue(rest.is_empty()) - match, rest = self._split_exception_group(eg, (ValueError, SyntaxError)) - checkMatch(eg, eg_template) - checkMatch(match, self._reduce(eg_template, ValueError)) - checkMatch(rest, self._reduce(eg_template, TypeError)) + # Match ValueErrors + match, rest = self._split_exception_group(eg, ValueError) + self.assertMatchesTemplate(match, + [ + [ + [ValueError(45), ValueError(55)], + [ValueError(65), ValueError(75)], + ValueError(75), + ], + ValueError(55) + ]) + self.assertMatchesTemplate( + rest, [[[TypeError(int)],[TypeError(int)]]]) + + # Match TypeErrors + match, rest = self._split_exception_group(eg, (TypeError, SyntaxError)) + self.assertMatchesTemplate( + match, [[[TypeError(int)],[TypeError(int)]]]) + self.assertMatchesTemplate(rest, + [ + [ + [ValueError(45), ValueError(55)], + [ValueError(65), ValueError(75)], + ValueError(75), + ], + ValueError(55) + ]) - match, rest = self._split_exception_group(eg, (ValueError, TypeError)) - checkMatch(eg, eg_template) - checkMatch(match, eg_template) - checkMatch(rest, []) class ExceptionGroupCatchTests(ExceptionGroupTestUtils): def checkMatch(self, exc, template, reference_tbs): From 0001e281b5157038d3a8d3c4653b94fd61144a4f Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Mon, 2 Nov 2020 18:01:06 +0000 Subject: [PATCH 40/73] make tests more explicit --- Lib/test/test_exception_group.py | 498 ++++++++++++------------------- 1 file changed, 195 insertions(+), 303 deletions(-) diff --git a/Lib/test/test_exception_group.py b/Lib/test/test_exception_group.py index 9f26de63c034f2..755bd70d163a8b 100644 --- a/Lib/test/test_exception_group.py +++ b/Lib/test/test_exception_group.py @@ -32,12 +32,6 @@ def tracebackGroupSanityCheck(self, exc): for e in exc.excs: self.tracebackGroupSanityCheck(e) - def to_template(self, exc): - if isinstance(exc, ExceptionGroup): - return [self.to_template(e) for e in exc.excs] - else: - return exc - class ExceptionGroupTestUtils(ExceptionGroupTestBase): def create_EG(self, raisers): @@ -58,33 +52,6 @@ def raiseValueError(self, v): def raiseTypeError(self, t): raise TypeError(t) - def get_test_exceptions(self, x): - return [ - (self.raiseValueError, ValueError, x+1), - (self.raiseTypeError, TypeError, 'int'), - (self.raiseValueError, ValueError, x+2), - (self.raiseValueError, ValueError, x+3), - (self.raiseTypeError, TypeError, 'list'), - ] - - def simple_exception_group(self, x): - excs = [] - for f, _, arg in self.get_test_exceptions(x): - try: - f(arg) - except Exception as e: - excs.append(e) - raise ExceptionGroup(excs) - - def nested_exception_group(self): - excs = [] - for x in [1,2,3]: - try: - self.simple_exception_group(x) - except ExceptionGroup as e: - excs.append(e) - raise ExceptionGroup(excs) - def funcname(self, tb_frame): return tb_frame.f_code.co_name @@ -96,37 +63,6 @@ def funcnames(self, tb): tb = tb.tb_next return names - def _reduce(self, template, types): - """ reduce a nested list of types to certain types - - The result is a nested list of the same shape as template, - but with only exceptions that match types - """ - if isinstance(template, collections.abc.Sequence): - res = [self._reduce(t, types) for t in template] - return [x for x in res if x is not None] - elif isinstance(template, types): - return template - else: - return None - -class ExceptionGroupTestUtilsTests(ExceptionGroupTestUtils): - def test_reduce(self): - te = TypeError('int') - se = SyntaxError('blah') - ve1 = ValueError(1) - ve2 = ValueError(2) - template = [[te, ve1], se, [ve2]] - reduce = self._reduce - self.assertEqual(reduce(template, ()), [[],[]]) - self.assertEqual(reduce(template, TypeError), [[te],[]]) - self.assertEqual(reduce(template, ValueError), [[ve1],[ve2]]) - self.assertEqual(reduce(template, SyntaxError), [[], se, []]) - self.assertEqual( - reduce(template, (TypeError, ValueError)), [[te, ve1], [ve2]]) - self.assertEqual( - reduce(template, (TypeError, SyntaxError)), [[te], se, []]) - class ExceptionGroupConstructionTests(ExceptionGroupTestUtils): def test_construction_simple(self): @@ -254,28 +190,27 @@ def _split_exception_group(self, eg, types): return match, rest def test_split_nested(self): - # create a nested exception group and check that - # it is constructed as expected + # create a nested exception group bind = functools.partial level1 = lambda i: self.create_EG([ bind(self.raiseValueError, i), bind(self.raiseTypeError, int), - bind(self.raiseValueError, i+10), + bind(self.raiseValueError, i+11), ]) def raiseException(e): raise e level2 = lambda i : self.create_EG([ bind(raiseException, level1(i)), - bind(raiseException, level1(i+20)), - bind(self.raiseValueError, i+30), + bind(raiseException, level1(i+22)), + bind(self.raiseValueError, i+33), ]) level3 = lambda i : self.create_EG([ - bind(raiseException, level2(i+40)), - bind(self.raiseValueError, i+50), + bind(raiseException, level2(i+44)), + bind(self.raiseValueError, i+55), ]) try: - raise level3(5) + raise level3(6) except ExceptionGroup as e: eg = e @@ -284,14 +219,26 @@ def raiseException(e): raise e eg_template = [ [ - [ValueError(45), TypeError(int), ValueError(55)], - [ValueError(65), TypeError(int), ValueError(75)], - ValueError(75), + [ValueError(50), TypeError(int), ValueError(61)], + [ValueError(72), TypeError(int), ValueError(83)], + ValueError(83), ], - ValueError(55) + ValueError(61) ] self.assertMatchesTemplate(eg, eg_template) + valueErrors_template = [ + [ + [ValueError(50), ValueError(61)], + [ValueError(72), ValueError(83)], + ValueError(83), + ], + ValueError(61) + ] + + typeErrors_template = [[[TypeError(int)],[TypeError(int)]]] + + # Match Nothing match, rest = self._split_exception_group(eg, SyntaxError) self.assertTrue(match.is_empty()) @@ -307,243 +254,228 @@ def raiseException(e): raise e # Match ValueErrors match, rest = self._split_exception_group(eg, ValueError) - self.assertMatchesTemplate(match, - [ - [ - [ValueError(45), ValueError(55)], - [ValueError(65), ValueError(75)], - ValueError(75), - ], - ValueError(55) - ]) - self.assertMatchesTemplate( - rest, [[[TypeError(int)],[TypeError(int)]]]) + self.assertMatchesTemplate(match, valueErrors_template) + self.assertMatchesTemplate(rest, typeErrors_template) # Match TypeErrors match, rest = self._split_exception_group(eg, (TypeError, SyntaxError)) - self.assertMatchesTemplate( - match, [[[TypeError(int)],[TypeError(int)]]]) - self.assertMatchesTemplate(rest, - [ - [ - [ValueError(45), ValueError(55)], - [ValueError(65), ValueError(75)], - ValueError(75), - ], - ValueError(55) - ]) + self.assertMatchesTemplate(match, typeErrors_template) + self.assertMatchesTemplate(rest, valueErrors_template) class ExceptionGroupCatchTests(ExceptionGroupTestUtils): - def checkMatch(self, exc, template, reference_tbs): + def setUp(self): + super().setUp() + + # create a nested exception group + bind = functools.partial + level1 = lambda i: self.create_EG([ + bind(self.raiseValueError, i), + bind(self.raiseTypeError, int), + bind(self.raiseValueError, i+10), + ]) + + def raiseException(e): raise e + level2 = lambda i : self.create_EG([ + bind(raiseException, level1(i)), + bind(raiseException, level1(i+20)), + bind(self.raiseValueError, i+30), + ]) + + level3 = lambda i : self.create_EG([ + bind(raiseException, level2(i+40)), + bind(self.raiseValueError, i+50), + ]) + try: + raise level3(5) + except ExceptionGroup as e: + self.eg = e + + fnames = ['setUp', 'create_EG'] + self.assertEqual(self.funcnames(self.eg.__traceback__), fnames) + + # templates + self.eg_template = [ + [ + [ValueError(45), TypeError(int), ValueError(55)], + [ValueError(65), TypeError(int), ValueError(75)], + ValueError(75), + ], + ValueError(55) + ] + + self.valueErrors_template = [ + [ + [ValueError(45), ValueError(55)], + [ValueError(65), ValueError(75)], + ValueError(75), + ], + ValueError(55) + ] + + self.typeErrors_template = [[[TypeError(int)],[TypeError(int)]]] + + + def checkMatch(self, exc, template): self.assertMatchesTemplate(exc, template) for e in exc: result = [self.funcname(f) for f in exc.extract_traceback(e)] - ref = reference_tbs[(type(e), e.args)] - # result has more frames from the Catcher context - # manager, ignore them - try: - result.remove('__exit__') - except ValueError: - pass - if result != ref: - self.assertEqual(result[-len(ref):], ref) - def test_catch_simple_eg_swallowing_handler(self): - try: - self.simple_exception_group(12) - except ExceptionGroup as eg: - ref_tbs = {} - for e in eg: - tb = [self.funcname(f) for f in eg.extract_traceback(e)] - ref_tbs[(type(e), e.args)] = tb - eg_template = self.to_template(eg) - def handler(eg): + def test_catch_handler_raises_subsets_of_caught(self): + eg = self.eg + eg_template = self.eg_template + valueErrors_template = self.valueErrors_template + typeErrors_template = self.typeErrors_template + + def handler(e): nonlocal caught - caught = eg + caught = e try: ######### Catch nothing: caught = raised = None with ExceptionGroup.catch(SyntaxError, handler): - self.simple_exception_group(12) - except ExceptionGroup as eg: - raised = eg - self.checkMatch(raised, eg_template, ref_tbs) + raise eg + except ExceptionGroup as e: + raised = e + self.checkMatch(raised, eg_template) self.assertIsNone(caught) try: ######### Catch everything: caught = None with ExceptionGroup.catch((ValueError, TypeError), handler): - self.simple_exception_group(12) + raise eg finally: - self.checkMatch(caught, eg_template, ref_tbs) + self.checkMatch(caught, eg_template) try: ######### Catch something: caught = raised = None with ExceptionGroup.catch(TypeError, handler): - self.simple_exception_group(12) - except ExceptionGroup as eg: - raised = eg - self.checkMatch(raised, self._reduce(eg_template, ValueError), ref_tbs) - self.checkMatch(caught, self._reduce(eg_template, TypeError), ref_tbs) + raise eg + except ExceptionGroup as e: + raised = e + self.checkMatch(raised, valueErrors_template) + self.checkMatch(caught, typeErrors_template) try: ######### Catch something: caught = raised = None with ExceptionGroup.catch((ValueError, SyntaxError), handler): - self.simple_exception_group(12) + raise eg except ExceptionGroup as eg: raised = eg - self.checkMatch(raised, self._reduce(eg_template, TypeError), ref_tbs) - self.checkMatch(caught, self._reduce(eg_template, ValueError), ref_tbs) + self.checkMatch(raised, typeErrors_template) + self.checkMatch(caught, valueErrors_template) - def test_catch_nested_eg_swallowing_handler(self): - try: - self.nested_exception_group() - except ExceptionGroup as eg: - ref_tbs = {} - for e in eg: - tb = [self.funcname(f) for f in eg.extract_traceback(e)] - ref_tbs[(type(e), e.args)] = tb - eg_template = self.to_template(eg) + def test_catch_handler_adds_new_exceptions(self): + # create a nested exception group + eg = self.eg + eg_template = self.eg_template + valueErrors_template = self.valueErrors_template + typeErrors_template = self.typeErrors_template def handler(eg): nonlocal caught caught = eg + return ExceptionGroup( + [ValueError('foo'), + ExceptionGroup( + [SyntaxError('bar'), ValueError('baz')])]) + + newErrors_template = [ + ValueError('foo'), [SyntaxError('bar'), ValueError('baz')]] try: ######### Catch nothing: caught = raised = None with ExceptionGroup.catch(SyntaxError, handler): - self.nested_exception_group() - except ExceptionGroup as eg: - raised = eg - self.checkMatch(raised, eg_template, ref_tbs) + raise eg + except ExceptionGroup as e: + raised = e + # handler is never called + self.checkMatch(raised, eg_template) self.assertIsNone(caught) try: ######### Catch everything: caught = None with ExceptionGroup.catch((ValueError, TypeError), handler): - self.nested_exception_group() - finally: - self.checkMatch(caught, eg_template, ref_tbs) + raise eg + except ExceptionGroup as e: + raised = e + self.checkMatch(raised, newErrors_template) + self.checkMatch(caught, eg_template) try: ######### Catch something: caught = raised = None with ExceptionGroup.catch(TypeError, handler): - self.nested_exception_group() - except ExceptionGroup as eg: - raised = eg - self.checkMatch(raised, self._reduce(eg_template, ValueError), ref_tbs) - self.checkMatch(caught, self._reduce(eg_template, TypeError), ref_tbs) + raise eg + except ExceptionGroup as e: + raised = e + self.checkMatch(raised, [valueErrors_template, newErrors_template]) + self.checkMatch(caught, typeErrors_template) try: ######### Catch something: caught = raised = None with ExceptionGroup.catch((ValueError, SyntaxError), handler): - self.nested_exception_group() + raise eg except ExceptionGroup as eg: raised = eg - self.checkMatch(raised, self._reduce(eg_template, TypeError), ref_tbs) - self.checkMatch(caught, self._reduce(eg_template, ValueError), ref_tbs) + self.checkMatch(raised, [typeErrors_template, newErrors_template]) + self.checkMatch(caught, valueErrors_template) + + + def test_catch_handler_reraise_all_matched(self): + eg = self.eg + eg_template = self.eg_template + valueErrors_template = self.valueErrors_template + typeErrors_template = self.typeErrors_template - def test_catch_nested_eg_handler_raises_new_exceptions(self): def handler(eg): nonlocal caught caught = eg - return ExceptionGroup( - [ValueError('foo'), - ExceptionGroup( - [SyntaxError('bar'), ValueError('baz')])]) - - try: - self.nested_exception_group() - except ExceptionGroup as eg: - eg1 = eg - eg_template = self.to_template(eg) - - try: - raise handler(None) - except ExceptionGroup as eg: - eg2 = eg - raised_template = self.to_template(eg) - - ref_tbs = {} - for eg in (eg1, eg2): - for e in eg: - tb = [self.funcname(f) for f in eg.extract_traceback(e)] - ref_tbs[(type(e), e.args)] = tb + return eg try: ######### Catch nothing: caught = raised = None with ExceptionGroup.catch(SyntaxError, handler): - self.nested_exception_group() - except ExceptionGroup as eg: - raised = eg - self.checkMatch(raised, eg_template, ref_tbs) + raise eg + except ExceptionGroup as e: + raised = e + # handler is never called + self.checkMatch(raised, eg_template) self.assertIsNone(caught) try: ######### Catch everything: caught = None with ExceptionGroup.catch((ValueError, TypeError), handler): - self.nested_exception_group() - except ExceptionGroup as eg: - raised = eg - self.checkMatch(raised, raised_template, ref_tbs) - self.checkMatch(caught, eg_template, ref_tbs) + raise eg + except ExceptionGroup as e: + raised = e + self.checkMatch(raised, eg_template) + self.checkMatch(caught, eg_template) try: ######### Catch something: caught = raised = None with ExceptionGroup.catch(TypeError, handler): - self.nested_exception_group() - except ExceptionGroup as eg: - raised = eg - self.checkMatch(raised, - [self._reduce(eg_template, ValueError), raised_template], - ref_tbs) - self.checkMatch(caught, self._reduce(eg_template, TypeError), ref_tbs) + raise eg + except ExceptionGroup as e: + raised = e + self.checkMatch(raised, eg_template) + self.checkMatch(caught, typeErrors_template) try: ######### Catch something: caught = raised = None with ExceptionGroup.catch((ValueError, SyntaxError), handler): - self.nested_exception_group() - except ExceptionGroup as eg: - raised = eg - self.checkMatch(raised, - [self._reduce(eg_template, TypeError), raised_template], - ref_tbs) - self.checkMatch(caught, self._reduce(eg_template, ValueError), ref_tbs) - - def test_catch_nested_eg_handler_reraise_all_matched(self): - def handler(eg): - return eg - - try: - self.nested_exception_group() - except ExceptionGroup as eg: - eg1 = eg - eg_template = self.to_template(eg) + raise eg + except ExceptionGroup as e: + raised = e + self.checkMatch(raised, eg_template) + self.checkMatch(caught, valueErrors_template) - ref_tbs = {} - for e in eg1: - tb = [self.funcname(f) for f in eg1.extract_traceback(e)] - ref_tbs[(type(e), e.args)] = tb + def test_catch_handler_reraise_new_and_all_old(self): + eg = self.eg + eg_template = self.eg_template + valueErrors_template = self.valueErrors_template + typeErrors_template = self.typeErrors_template - try: ######### Catch TypeErrors: - raised = None - with ExceptionGroup.catch(TypeError, handler): - self.nested_exception_group() - except ExceptionGroup as eg: - raised = eg - self.checkMatch(raised, eg_template, ref_tbs) - - try: ######### Catch ValueErrors: - raised = None - with ExceptionGroup.catch((ValueError, SyntaxError), handler): - self.nested_exception_group() - except ExceptionGroup as eg: - raised = eg - self.checkMatch(raised, eg_template, ref_tbs) - - def test_catch_nested_eg_handler_reraise_new_and_all_old(self): def handler(eg): return ExceptionGroup( [eg, @@ -551,99 +483,59 @@ def handler(eg): ExceptionGroup( [SyntaxError('bar'), ValueError('baz')])]) - try: - self.nested_exception_group() - except ExceptionGroup as eg: - eg1 = eg - eg_template = self.to_template(eg) - - class DummyException(Exception): pass - try: - raise handler(DummyException()) - except ExceptionGroup as eg: - _, eg2 = eg.split(DummyException) - new_raised_template = self.to_template(eg2) - - ref_tbs = {} - for eg in (eg1, eg2): - for e in eg: - tb = [self.funcname(f) for f in eg.extract_traceback(e)] - ref_tbs[(type(e), e.args)] = tb + newErrors_template = [ + ValueError('foo'), [SyntaxError('bar'), ValueError('baz')]] try: ######### Catch TypeErrors: raised = None with ExceptionGroup.catch(TypeError, handler): - self.nested_exception_group() - except ExceptionGroup as eg: - raised = eg - self.checkMatch(raised, [eg_template, new_raised_template], ref_tbs) + raise eg + except ExceptionGroup as e: + raised = e + self.checkMatch(raised, [eg_template, newErrors_template]) try: ######### Catch ValueErrors: raised = None with ExceptionGroup.catch((ValueError, SyntaxError), handler): - self.nested_exception_group() - except ExceptionGroup as eg: - raised = eg - self.checkMatch(raised, [eg_template, new_raised_template], ref_tbs) + raise eg + except ExceptionGroup as e: + raised = e + self.checkMatch(raised, [eg_template, newErrors_template]) + + def test_catch_handler_reraise_new_and_some_old(self): + eg = self.eg + eg_template = self.eg_template + valueErrors_template = self.valueErrors_template + typeErrors_template = self.typeErrors_template - def test_catch_nested_eg_handler_reraise_new_and_some_old(self): def handler(eg): ret = ExceptionGroup( - [eg.excs[1], + [eg.excs[0], ValueError('foo'), ExceptionGroup( [SyntaxError('bar'), ValueError('baz')])]) return ret - try: - self.nested_exception_group() - except ExceptionGroup as eg: - eg1 = eg - eg_template = self.to_template(eg) - - class DummyException(Exception): pass - try: - eg = ExceptionGroup([DummyException(), DummyException()]) - raise handler(eg) - except ExceptionGroup as eg: - _, eg2 = eg.split(DummyException) - new_raised_template = self.to_template(eg2) - - ref_tbs = {} - for eg in (eg1, eg2): - for e in eg: - tb = [self.funcname(f) for f in eg.extract_traceback(e)] - ref_tbs[(type(e), e.args)] = tb + newErrors_template = [ + ValueError('foo'), [SyntaxError('bar'), ValueError('baz')]] try: ######### Catch TypeErrors: raised = None with ExceptionGroup.catch(TypeError, handler): - self.nested_exception_group() - except ExceptionGroup as eg: - raised = eg - self.checkMatch(raised, - [ - [ self._reduce(eg_template[0], ValueError), - eg_template[1], - self._reduce(eg_template[2], ValueError), - ], - new_raised_template], - ref_tbs) + raise eg + except ExceptionGroup as e: + raised = e + # all TypeError are in eg.excs[0] so everything was reraised + self.checkMatch(raised, [eg_template, newErrors_template]) try: ######### Catch ValueErrors: raised = None with ExceptionGroup.catch((ValueError, SyntaxError), handler): - self.nested_exception_group() - except ExceptionGroup as eg: - raised = eg - self.checkMatch(raised, - [ - [ self._reduce(eg_template[0], TypeError), - eg_template[1], - self._reduce(eg_template[2], TypeError), - ], - new_raised_template], - ref_tbs) + raise eg + except ExceptionGroup as e: + raised = e + # eg.excs[0] is reraised and eg.excs[1] is consumed + self.checkMatch(raised, [[eg_template[0]], newErrors_template]) if __name__ == '__main__': unittest.main() From b11a262d33e4d6a1442f06dace84173d68eda7ab Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Mon, 2 Nov 2020 18:12:21 +0000 Subject: [PATCH 41/73] call super.__init__ from ExceptionGroup.__init__ and give it the message --- Lib/exception_group.py | 12 ++++++------ Lib/test/test_exception_group.py | 10 +++++++--- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/Lib/exception_group.py b/Lib/exception_group.py index 376dadb7253e79..61dfadba6758f4 100644 --- a/Lib/exception_group.py +++ b/Lib/exception_group.py @@ -19,8 +19,7 @@ def __init__(self, excs): class ExceptionGroup(BaseException): - def __init__(self, excs, *, msg=None, tb=None): - # TODO: msg arg comes first + def __init__(self, excs, message=None, *, tb=None): """ Construct a new ExceptionGroup excs: sequence of exceptions @@ -28,7 +27,8 @@ def __init__(self, excs, *, msg=None, tb=None): Typically set when this ExceptionGroup is derived from another. """ self.excs = excs - self.msg = msg + self.message = message + super().__init__(self.message) # self.__traceback__ is updated as usual, but self.__traceback_group__ # is set when the exception group is created. # __traceback_group__ and __traceback__ combine to give the full path. @@ -43,7 +43,7 @@ def project(self, condition, with_complement=False): returns another ExceptionGroup for the exception for which condition returns False. match and rest have the same nested structure as self, but empty - sub-exceptions are not included. They have the same msg, + sub-exceptions are not included. They have the same message, __traceback__, __cause__ and __context__ fields as self. condition: BaseException --> Boolean @@ -67,7 +67,7 @@ def project(self, condition, with_complement=False): match_exc = ExceptionGroup(match, tb=self.__traceback__) def copy_metadata(src, target): - target.msg = src.msg + target.message = src.message target.__context__ = src.__context__ target.__cause__ = src.__cause__ copy_metadata(self, match_exc) @@ -216,7 +216,7 @@ def __exit__(self, etype, exc, tb): [e for e in handler_excs if e not in match]) if not to_add.is_empty(): to_raise = ExceptionGroup([to_keep, to_add]) - to_raise.msg = exc.msg + to_raise.message = exc.message else: to_raise = to_keep diff --git a/Lib/test/test_exception_group.py b/Lib/test/test_exception_group.py index 755bd70d163a8b..0a03918d42e573 100644 --- a/Lib/test/test_exception_group.py +++ b/Lib/test/test_exception_group.py @@ -34,7 +34,7 @@ def tracebackGroupSanityCheck(self, exc): class ExceptionGroupTestUtils(ExceptionGroupTestBase): - def create_EG(self, raisers): + def create_EG(self, raisers, message=None): excs = [] for r in raisers: try: @@ -42,7 +42,7 @@ def create_EG(self, raisers): except (Exception, ExceptionGroup) as e: excs.append(e) try: - raise ExceptionGroup(excs) + raise ExceptionGroup(excs, message=message) except ExceptionGroup as e: return e @@ -73,7 +73,7 @@ def test_construction_simple(self): [bind(self.raiseValueError, 1), bind(self.raiseTypeError, int), bind(self.raiseValueError, 2), - ]) + ], message='hello world') self.assertEqual(len(eg.excs), 3) self.assertMatchesTemplate(eg, @@ -82,6 +82,10 @@ def test_construction_simple(self): # check iteration self.assertEqual(list(eg), list(eg.excs)) + # check message + self.assertEqual(eg.message, 'hello world') + self.assertEqual(eg.args, ('hello world',)) + # check tracebacks for e in eg: expected = [ From d9929ff06e78741e0671acae73e60dcff5ffb5e6 Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Mon, 2 Nov 2020 18:35:24 +0000 Subject: [PATCH 42/73] fix test name --- Lib/test/test_exception_group.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_exception_group.py b/Lib/test/test_exception_group.py index 0a03918d42e573..8eec211615f6e6 100644 --- a/Lib/test/test_exception_group.py +++ b/Lib/test/test_exception_group.py @@ -326,7 +326,7 @@ def checkMatch(self, exc, template): result = [self.funcname(f) for f in exc.extract_traceback(e)] - def test_catch_handler_raises_subsets_of_caught(self): + def test_catch_handler_raises_nothing(self): eg = self.eg eg_template = self.eg_template valueErrors_template = self.valueErrors_template From d4e44cfd87febf9def2182b7fc0e8ce12f920759 Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Mon, 2 Nov 2020 18:40:22 +0000 Subject: [PATCH 43/73] Change handler API - return True for naked raise or raise some EG --- Lib/exception_group.py | 10 ++- Lib/test/test_exception_group.py | 102 ++++++++++++++++--------------- 2 files changed, 62 insertions(+), 50 deletions(-) diff --git a/Lib/exception_group.py b/Lib/exception_group.py index 61dfadba6758f4..6912d4a703ff2c 100644 --- a/Lib/exception_group.py +++ b/Lib/exception_group.py @@ -189,8 +189,14 @@ def __exit__(self, etype, exc, tb): # Let the interpreter reraise the exception return False - handler_excs = self.handler(match) - if handler_excs is match: + naked_raise = False + handler_excs = None + try: + naked_raise = self.handler(match) + except (Exception, ExceptionGroup) as e: + handler_excs = e + + if naked_raise or handler_excs is match: # handler reraised all of the matched exceptions. # reraise exc as is. return False diff --git a/Lib/test/test_exception_group.py b/Lib/test/test_exception_group.py index 8eec211615f6e6..d095b3534b98c0 100644 --- a/Lib/test/test_exception_group.py +++ b/Lib/test/test_exception_group.py @@ -380,10 +380,10 @@ def test_catch_handler_adds_new_exceptions(self): def handler(eg): nonlocal caught caught = eg - return ExceptionGroup( - [ValueError('foo'), - ExceptionGroup( - [SyntaxError('bar'), ValueError('baz')])]) + raise ExceptionGroup( + [ValueError('foo'), + ExceptionGroup( + [SyntaxError('bar'), ValueError('baz')])]) newErrors_template = [ ValueError('foo'), [SyntaxError('bar'), ValueError('baz')]] @@ -432,47 +432,54 @@ def test_catch_handler_reraise_all_matched(self): valueErrors_template = self.valueErrors_template typeErrors_template = self.typeErrors_template - def handler(eg): + # There are two ways to do this + def handler1(eg): nonlocal caught caught = eg - return eg - - try: ######### Catch nothing: - caught = raised = None - with ExceptionGroup.catch(SyntaxError, handler): - raise eg - except ExceptionGroup as e: - raised = e - # handler is never called - self.checkMatch(raised, eg_template) - self.assertIsNone(caught) - - try: ######### Catch everything: - caught = None - with ExceptionGroup.catch((ValueError, TypeError), handler): - raise eg - except ExceptionGroup as e: - raised = e - self.checkMatch(raised, eg_template) - self.checkMatch(caught, eg_template) + return True - try: ######### Catch something: - caught = raised = None - with ExceptionGroup.catch(TypeError, handler): - raise eg - except ExceptionGroup as e: - raised = e - self.checkMatch(raised, eg_template) - self.checkMatch(caught, typeErrors_template) + def handler2(eg): + nonlocal caught + caught = eg + raise eg + + for handler in [handler1, handler2]: + try: ######### Catch nothing: + caught = raised = None + with ExceptionGroup.catch(SyntaxError, handler): + raise eg + except ExceptionGroup as e: + raised = e + # handler is never called + self.checkMatch(raised, eg_template) + self.assertIsNone(caught) + + try: ######### Catch everything: + caught = None + with ExceptionGroup.catch((ValueError, TypeError), handler): + raise eg + except ExceptionGroup as e: + raised = e + self.checkMatch(raised, eg_template) + self.checkMatch(caught, eg_template) - try: ######### Catch something: - caught = raised = None - with ExceptionGroup.catch((ValueError, SyntaxError), handler): - raise eg - except ExceptionGroup as e: - raised = e - self.checkMatch(raised, eg_template) - self.checkMatch(caught, valueErrors_template) + try: ######### Catch something: + caught = raised = None + with ExceptionGroup.catch(TypeError, handler): + raise eg + except ExceptionGroup as e: + raised = e + self.checkMatch(raised, eg_template) + self.checkMatch(caught, typeErrors_template) + + try: ######### Catch something: + caught = raised = None + with ExceptionGroup.catch((ValueError, SyntaxError), handler): + raise eg + except ExceptionGroup as e: + raised = e + self.checkMatch(raised, eg_template) + self.checkMatch(caught, valueErrors_template) def test_catch_handler_reraise_new_and_all_old(self): eg = self.eg @@ -481,11 +488,11 @@ def test_catch_handler_reraise_new_and_all_old(self): typeErrors_template = self.typeErrors_template def handler(eg): - return ExceptionGroup( - [eg, - ValueError('foo'), - ExceptionGroup( - [SyntaxError('bar'), ValueError('baz')])]) + raise ExceptionGroup( + [eg, + ValueError('foo'), + ExceptionGroup( + [SyntaxError('bar'), ValueError('baz')])]) newErrors_template = [ ValueError('foo'), [SyntaxError('bar'), ValueError('baz')]] @@ -513,12 +520,11 @@ def test_catch_handler_reraise_new_and_some_old(self): typeErrors_template = self.typeErrors_template def handler(eg): - ret = ExceptionGroup( + raise ExceptionGroup( [eg.excs[0], ValueError('foo'), ExceptionGroup( [SyntaxError('bar'), ValueError('baz')])]) - return ret newErrors_template = [ ValueError('foo'), [SyntaxError('bar'), ValueError('baz')]] From 2d96ffa6096814717ced4f0d721c934829dc3231 Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Mon, 2 Nov 2020 18:52:41 +0000 Subject: [PATCH 44/73] In catcher tests, compare caught exception's traceback to that of the original exception. --- Lib/test/test_exception_group.py | 65 +++++++++++++++++--------------- 1 file changed, 34 insertions(+), 31 deletions(-) diff --git a/Lib/test/test_exception_group.py b/Lib/test/test_exception_group.py index d095b3534b98c0..2c51625170c4ff 100644 --- a/Lib/test/test_exception_group.py +++ b/Lib/test/test_exception_group.py @@ -320,11 +320,14 @@ def raiseException(e): raise e self.typeErrors_template = [[[TypeError(int)],[TypeError(int)]]] - def checkMatch(self, exc, template): + def checkMatch(self, exc, template, orig_eg): self.assertMatchesTemplate(exc, template) for e in exc: - result = [self.funcname(f) for f in exc.extract_traceback(e)] - + f_data = lambda f: [f.f_code.co_name, f.f_lineno] + new = list(map(f_data, exc.extract_traceback(e))) + if e in orig_eg: + old = list(map(f_data, orig_eg.extract_traceback(e))) + self.assertSequenceEqual(old, new[-len(old):]) def test_catch_handler_raises_nothing(self): eg = self.eg @@ -342,7 +345,7 @@ def handler(e): raise eg except ExceptionGroup as e: raised = e - self.checkMatch(raised, eg_template) + self.checkMatch(raised, eg_template, eg) self.assertIsNone(caught) try: ######### Catch everything: @@ -350,7 +353,7 @@ def handler(e): with ExceptionGroup.catch((ValueError, TypeError), handler): raise eg finally: - self.checkMatch(caught, eg_template) + self.checkMatch(caught, eg_template, eg) try: ######### Catch something: caught = raised = None @@ -358,17 +361,17 @@ def handler(e): raise eg except ExceptionGroup as e: raised = e - self.checkMatch(raised, valueErrors_template) - self.checkMatch(caught, typeErrors_template) + self.checkMatch(raised, valueErrors_template, eg) + self.checkMatch(caught, typeErrors_template, eg) try: ######### Catch something: caught = raised = None with ExceptionGroup.catch((ValueError, SyntaxError), handler): raise eg - except ExceptionGroup as eg: - raised = eg - self.checkMatch(raised, typeErrors_template) - self.checkMatch(caught, valueErrors_template) + except ExceptionGroup as e: + raised = e + self.checkMatch(raised, typeErrors_template, eg) + self.checkMatch(caught, valueErrors_template, eg) def test_catch_handler_adds_new_exceptions(self): # create a nested exception group @@ -395,7 +398,7 @@ def handler(eg): except ExceptionGroup as e: raised = e # handler is never called - self.checkMatch(raised, eg_template) + self.checkMatch(raised, eg_template, eg) self.assertIsNone(caught) try: ######### Catch everything: @@ -404,8 +407,8 @@ def handler(eg): raise eg except ExceptionGroup as e: raised = e - self.checkMatch(raised, newErrors_template) - self.checkMatch(caught, eg_template) + self.checkMatch(raised, newErrors_template, eg) + self.checkMatch(caught, eg_template, eg) try: ######### Catch something: caught = raised = None @@ -413,17 +416,17 @@ def handler(eg): raise eg except ExceptionGroup as e: raised = e - self.checkMatch(raised, [valueErrors_template, newErrors_template]) - self.checkMatch(caught, typeErrors_template) + self.checkMatch(raised, [valueErrors_template, newErrors_template], eg) + self.checkMatch(caught, typeErrors_template, eg) try: ######### Catch something: caught = raised = None with ExceptionGroup.catch((ValueError, SyntaxError), handler): raise eg - except ExceptionGroup as eg: - raised = eg - self.checkMatch(raised, [typeErrors_template, newErrors_template]) - self.checkMatch(caught, valueErrors_template) + except ExceptionGroup as e: + raised = e + self.checkMatch(raised, [typeErrors_template, newErrors_template], eg) + self.checkMatch(caught, valueErrors_template, eg) def test_catch_handler_reraise_all_matched(self): @@ -451,7 +454,7 @@ def handler2(eg): except ExceptionGroup as e: raised = e # handler is never called - self.checkMatch(raised, eg_template) + self.checkMatch(raised, eg_template, eg) self.assertIsNone(caught) try: ######### Catch everything: @@ -460,8 +463,8 @@ def handler2(eg): raise eg except ExceptionGroup as e: raised = e - self.checkMatch(raised, eg_template) - self.checkMatch(caught, eg_template) + self.checkMatch(raised, eg_template, eg) + self.checkMatch(caught, eg_template, eg) try: ######### Catch something: caught = raised = None @@ -469,8 +472,8 @@ def handler2(eg): raise eg except ExceptionGroup as e: raised = e - self.checkMatch(raised, eg_template) - self.checkMatch(caught, typeErrors_template) + self.checkMatch(raised, eg_template, eg) + self.checkMatch(caught, typeErrors_template, eg) try: ######### Catch something: caught = raised = None @@ -478,8 +481,8 @@ def handler2(eg): raise eg except ExceptionGroup as e: raised = e - self.checkMatch(raised, eg_template) - self.checkMatch(caught, valueErrors_template) + self.checkMatch(raised, eg_template, eg) + self.checkMatch(caught, valueErrors_template, eg) def test_catch_handler_reraise_new_and_all_old(self): eg = self.eg @@ -503,7 +506,7 @@ def handler(eg): raise eg except ExceptionGroup as e: raised = e - self.checkMatch(raised, [eg_template, newErrors_template]) + self.checkMatch(raised, [eg_template, newErrors_template], eg) try: ######### Catch ValueErrors: raised = None @@ -511,7 +514,7 @@ def handler(eg): raise eg except ExceptionGroup as e: raised = e - self.checkMatch(raised, [eg_template, newErrors_template]) + self.checkMatch(raised, [eg_template, newErrors_template], eg) def test_catch_handler_reraise_new_and_some_old(self): eg = self.eg @@ -536,7 +539,7 @@ def handler(eg): except ExceptionGroup as e: raised = e # all TypeError are in eg.excs[0] so everything was reraised - self.checkMatch(raised, [eg_template, newErrors_template]) + self.checkMatch(raised, [eg_template, newErrors_template], eg) try: ######### Catch ValueErrors: raised = None @@ -545,7 +548,7 @@ def handler(eg): except ExceptionGroup as e: raised = e # eg.excs[0] is reraised and eg.excs[1] is consumed - self.checkMatch(raised, [[eg_template[0]], newErrors_template]) + self.checkMatch(raised, [[eg_template[0]], newErrors_template], eg) if __name__ == '__main__': unittest.main() From 3581143f04f366ebb824b33faaefaef3cb62066e Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Tue, 3 Nov 2020 13:51:34 +0000 Subject: [PATCH 45/73] reduce boilerplate in tests --- Lib/test/test_exception_group.py | 210 +++++++++++++------------------ 1 file changed, 86 insertions(+), 124 deletions(-) diff --git a/Lib/test/test_exception_group.py b/Lib/test/test_exception_group.py index 2c51625170c4ff..2182db9bf2ba4f 100644 --- a/Lib/test/test_exception_group.py +++ b/Lib/test/test_exception_group.py @@ -329,47 +329,53 @@ def checkMatch(self, exc, template, orig_eg): old = list(map(f_data, orig_eg.extract_traceback(e))) self.assertSequenceEqual(old, new[-len(old):]) + class BaseHandler: + def __init__(self): + self.caught = None + + def __call__(self, eg): + self.caught = eg + return self.handle(eg) + + def apply_catcher(self, catch, handler_cls, eg): + try: + raised = None + handler = handler_cls() + with ExceptionGroup.catch(catch, handler): + raise eg + except ExceptionGroup as e: + raised = e + return handler.caught, raised + def test_catch_handler_raises_nothing(self): eg = self.eg eg_template = self.eg_template valueErrors_template = self.valueErrors_template typeErrors_template = self.typeErrors_template - def handler(e): - nonlocal caught - caught = e + class Handler(self.BaseHandler): + def handle(self, eg): + pass - try: ######### Catch nothing: - caught = raised = None - with ExceptionGroup.catch(SyntaxError, handler): - raise eg - except ExceptionGroup as e: - raised = e + ######### Catch nothing: + caught, raised = self.apply_catcher(SyntaxError, Handler, eg) self.checkMatch(raised, eg_template, eg) self.assertIsNone(caught) - try: ######### Catch everything: - caught = None - with ExceptionGroup.catch((ValueError, TypeError), handler): - raise eg - finally: - self.checkMatch(caught, eg_template, eg) + ######### Catch everything: + error_types = (ValueError, TypeError) + caught, raised = self.apply_catcher(error_types, Handler, eg) + self.assertIsNone(raised) + self.checkMatch(caught, eg_template, eg) - try: ######### Catch something: - caught = raised = None - with ExceptionGroup.catch(TypeError, handler): - raise eg - except ExceptionGroup as e: - raised = e + ######### Catch TypeErrors: + caught, raised = self.apply_catcher(TypeError, Handler, eg) self.checkMatch(raised, valueErrors_template, eg) self.checkMatch(caught, typeErrors_template, eg) - try: ######### Catch something: - caught = raised = None - with ExceptionGroup.catch((ValueError, SyntaxError), handler): - raise eg - except ExceptionGroup as e: - raised = e + ######### Catch ValueErrors: + error_types = (ValueError, SyntaxError) + caught, raised = self.apply_catcher(error_types, Handler, eg) self.checkMatch(raised, typeErrors_template, eg) self.checkMatch(caught, valueErrors_template, eg) @@ -380,10 +386,9 @@ def test_catch_handler_adds_new_exceptions(self): valueErrors_template = self.valueErrors_template typeErrors_template = self.typeErrors_template - def handler(eg): - nonlocal caught - caught = eg - raise ExceptionGroup( + class Handler(self.BaseHandler): + def handle(self, eg): + raise ExceptionGroup( [ValueError('foo'), ExceptionGroup( [SyntaxError('bar'), ValueError('baz')])]) @@ -391,40 +396,24 @@ def handler(eg): newErrors_template = [ ValueError('foo'), [SyntaxError('bar'), ValueError('baz')]] - try: ######### Catch nothing: - caught = raised = None - with ExceptionGroup.catch(SyntaxError, handler): - raise eg - except ExceptionGroup as e: - raised = e - # handler is never called + ######### Catch nothing: + caught, raised = self.apply_catcher(SyntaxError, Handler, eg) self.checkMatch(raised, eg_template, eg) self.assertIsNone(caught) - try: ######### Catch everything: - caught = None - with ExceptionGroup.catch((ValueError, TypeError), handler): - raise eg - except ExceptionGroup as e: - raised = e + ######### Catch everything: + error_types = (ValueError, TypeError) + caught, raised = self.apply_catcher(error_types, Handler, eg) self.checkMatch(raised, newErrors_template, eg) self.checkMatch(caught, eg_template, eg) - try: ######### Catch something: - caught = raised = None - with ExceptionGroup.catch(TypeError, handler): - raise eg - except ExceptionGroup as e: - raised = e + ######### Catch TypeErrors: + caught, raised = self.apply_catcher(TypeError, Handler, eg) self.checkMatch(raised, [valueErrors_template, newErrors_template], eg) self.checkMatch(caught, typeErrors_template, eg) - try: ######### Catch something: - caught = raised = None - with ExceptionGroup.catch((ValueError, SyntaxError), handler): - raise eg - except ExceptionGroup as e: - raised = e + ######### Catch ValueErrors: + caught, raised = self.apply_catcher((ValueError, OSError), Handler, eg) self.checkMatch(raised, [typeErrors_template, newErrors_template], eg) self.checkMatch(caught, valueErrors_template, eg) @@ -436,51 +425,35 @@ def test_catch_handler_reraise_all_matched(self): typeErrors_template = self.typeErrors_template # There are two ways to do this - def handler1(eg): - nonlocal caught - caught = eg - return True - - def handler2(eg): - nonlocal caught - caught = eg - raise eg - - for handler in [handler1, handler2]: - try: ######### Catch nothing: - caught = raised = None - with ExceptionGroup.catch(SyntaxError, handler): - raise eg - except ExceptionGroup as e: - raised = e + class Handler1(self.BaseHandler): + def handle(self, eg): + return True + + class Handler2(self.BaseHandler): + def handle(self, eg): + raise eg + + for handler in [Handler1, Handler2]: + ######### Catch nothing: + caught, raised = self.apply_catcher(SyntaxError, handler, eg) # handler is never called self.checkMatch(raised, eg_template, eg) self.assertIsNone(caught) - try: ######### Catch everything: - caught = None - with ExceptionGroup.catch((ValueError, TypeError), handler): - raise eg - except ExceptionGroup as e: - raised = e + ######### Catch everything: + error_types = (ValueError, TypeError) + caught, raised = self.apply_catcher(error_types, handler, eg) self.checkMatch(raised, eg_template, eg) self.checkMatch(caught, eg_template, eg) - try: ######### Catch something: - caught = raised = None - with ExceptionGroup.catch(TypeError, handler): - raise eg - except ExceptionGroup as e: - raised = e + ######### Catch TypeErrors: + caught, raised = self.apply_catcher(TypeError, handler, eg) self.checkMatch(raised, eg_template, eg) self.checkMatch(caught, typeErrors_template, eg) - try: ######### Catch something: - caught = raised = None - with ExceptionGroup.catch((ValueError, SyntaxError), handler): - raise eg - except ExceptionGroup as e: - raised = e + ######### Catch ValueErrors: + catch = (ValueError, SyntaxError) + caught, raised = self.apply_catcher(catch, handler, eg) self.checkMatch(raised, eg_template, eg) self.checkMatch(caught, valueErrors_template, eg) @@ -490,8 +463,9 @@ def test_catch_handler_reraise_new_and_all_old(self): valueErrors_template = self.valueErrors_template typeErrors_template = self.typeErrors_template - def handler(eg): - raise ExceptionGroup( + class Handler(self.BaseHandler): + def handle(self, eg): + raise ExceptionGroup( [eg, ValueError('foo'), ExceptionGroup( @@ -500,21 +474,15 @@ def handler(eg): newErrors_template = [ ValueError('foo'), [SyntaxError('bar'), ValueError('baz')]] - try: ######### Catch TypeErrors: - raised = None - with ExceptionGroup.catch(TypeError, handler): - raise eg - except ExceptionGroup as e: - raised = e + ######### Catch TypeErrors: + caught, raised = self.apply_catcher(TypeError, Handler, eg) self.checkMatch(raised, [eg_template, newErrors_template], eg) + self.checkMatch(caught, typeErrors_template, eg) - try: ######### Catch ValueErrors: - raised = None - with ExceptionGroup.catch((ValueError, SyntaxError), handler): - raise eg - except ExceptionGroup as e: - raised = e + ######### Catch ValueErrors: + caught, raised = self.apply_catcher(ValueError, Handler, eg) self.checkMatch(raised, [eg_template, newErrors_template], eg) + self.checkMatch(caught, valueErrors_template, eg) def test_catch_handler_reraise_new_and_some_old(self): eg = self.eg @@ -522,33 +490,27 @@ def test_catch_handler_reraise_new_and_some_old(self): valueErrors_template = self.valueErrors_template typeErrors_template = self.typeErrors_template - def handler(eg): - raise ExceptionGroup( - [eg.excs[0], - ValueError('foo'), - ExceptionGroup( - [SyntaxError('bar'), ValueError('baz')])]) + class Handler(self.BaseHandler): + def handle(self, eg): + raise ExceptionGroup( + [eg.excs[0], + ValueError('foo'), + ExceptionGroup( + [SyntaxError('bar'), ValueError('baz')])]) newErrors_template = [ ValueError('foo'), [SyntaxError('bar'), ValueError('baz')]] - try: ######### Catch TypeErrors: - raised = None - with ExceptionGroup.catch(TypeError, handler): - raise eg - except ExceptionGroup as e: - raised = e - # all TypeError are in eg.excs[0] so everything was reraised + ######### Catch TypeErrors: + caught, raised = self.apply_catcher(TypeError, Handler, eg) self.checkMatch(raised, [eg_template, newErrors_template], eg) + self.checkMatch(caught, typeErrors_template, eg) - try: ######### Catch ValueErrors: - raised = None - with ExceptionGroup.catch((ValueError, SyntaxError), handler): - raise eg - except ExceptionGroup as e: - raised = e + ######### Catch ValueErrors: + caught, raised = self.apply_catcher(ValueError, Handler, eg) # eg.excs[0] is reraised and eg.excs[1] is consumed self.checkMatch(raised, [[eg_template[0]], newErrors_template], eg) + self.checkMatch(caught, valueErrors_template, eg) if __name__ == '__main__': unittest.main() From c243e94c38be71e67189c98675aa493761149656 Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Tue, 3 Nov 2020 22:37:08 +0000 Subject: [PATCH 46/73] use traceback.py types in ExceptionGroup.render/format/extract --- Lib/exception_group.py | 81 +++++---- Lib/test/test_exception_group.py | 279 +++++++++++++++++++------------ 2 files changed, 219 insertions(+), 141 deletions(-) diff --git a/Lib/exception_group.py b/Lib/exception_group.py index 6912d4a703ff2c..0067d4559cf445 100644 --- a/Lib/exception_group.py +++ b/Lib/exception_group.py @@ -1,5 +1,7 @@ import sys +import textwrap +import traceback class TracebackGroup: @@ -99,49 +101,43 @@ def extract_traceback(self, exc): """ returns the traceback of a single exception If exc is in this exception group, return its - traceback as a list of frames. Otherwise, return None. + traceback as the concatenation of the outputs + of traceback.extract_tb() on each segment of + it traceback (one per each ExceptionGroup that + it belongs to). """ - # TODO: integrate into traceback.py style - # TODO: return a traceback.StackSummary ? if exc not in self: return None - result = [] e = self.subgroup([exc]) - while e is not None and\ - (not isinstance(e, ExceptionGroup) or not e.is_empty()): - - tb = e.__traceback__ - while tb is not None: - result.append(tb.tb_frame) - tb = tb.tb_next + result = None + while e is not None: if isinstance(e, ExceptionGroup): - assert len(e.excs) == 1 and exc in e - e = e.excs[0] + assert len(e.excs) == 1 and exc in e + r = traceback.extract_tb(e.__traceback__) + if result is not None: + result.extend(r) else: - assert e is exc - e = None + result = r + e = e.excs[0] if isinstance(e, ExceptionGroup) else None return result @staticmethod - def render(exc, tb=None, indent=0): - # TODO: integrate into traceback.py style - output = [] - output.append(f"{exc!r}") - tb = tb or exc.__traceback__ - while tb is not None and not isinstance(tb, TracebackGroup): - output.append(f"{' '*indent} {tb.tb_frame}") - tb = tb.tb_next - if isinstance(exc, ExceptionGroup): - tbg = exc.__traceback_group__ - assert isinstance(tbg, TracebackGroup) - indent += 4 - for e in exc.excs: - t = tbg.tb_next_map[id(e)] - output.append('---------------------------------------') - output.extend(ExceptionGroup.render(e, t, indent)) - for l in output: - print(l) - return output + def format(exc, **kwargs): + result = [] + summary = StackGroupSummary.extract(exc, **kwargs) + for indent, exc_str, stack in summary: + prefix = ' '*indent + result.append(f"{prefix}{exc_str}:") + stack = traceback.StackSummary.format(stack) + result.extend([textwrap.indent(l.rstrip(), prefix) for l in stack]) + return result + + @staticmethod + def render(exc, file=None, **kwargs): + if file is None: + file = sys.stderr + for line in ExceptionGroup.format(exc, **kwargs): + print(line, file=file) def __iter__(self): ''' iterate over the individual exceptions (flattens the tree) ''' @@ -162,6 +158,23 @@ def __repr__(self): def catch(types, handler): return ExceptionGroupCatcher(types, handler) +class StackGroupSummary(list): + @classmethod + def extract(klass, exc, *, indent=0, result=None, + limit=None, lookup_lines=True, capture_locals=False): + + if result is None: + result = klass() + result.append([ + indent, + f"{exc!r}", + traceback.extract_tb(exc.__traceback__, limit=limit)]) + if isinstance(exc, ExceptionGroup): + for e in exc.excs: + StackGroupSummary.extract( + e, indent=indent+4, result=result, limit=limit) + return result + class ExceptionGroupCatcher: """ Based on trio.MultiErrorCatcher """ diff --git a/Lib/test/test_exception_group.py b/Lib/test/test_exception_group.py index 2182db9bf2ba4f..087dbe0e1d646a 100644 --- a/Lib/test/test_exception_group.py +++ b/Lib/test/test_exception_group.py @@ -1,8 +1,9 @@ +import collections.abc import functools +import traceback import unittest -import collections.abc -from exception_group import ExceptionGroup, TracebackGroup +from exception_group import ExceptionGroup, TracebackGroup, StackGroupSummary class ExceptionGroupTestBase(unittest.TestCase): @@ -34,7 +35,7 @@ def tracebackGroupSanityCheck(self, exc): class ExceptionGroupTestUtils(ExceptionGroupTestBase): - def create_EG(self, raisers, message=None): + def newEG(self, raisers, message=None): excs = [] for r in raisers: try: @@ -46,34 +47,23 @@ def create_EG(self, raisers, message=None): except ExceptionGroup as e: return e - def raiseValueError(self, v): + def newVE(self, v): raise ValueError(v) - def raiseTypeError(self, t): + def newTE(self, t): raise TypeError(t) - def funcname(self, tb_frame): - return tb_frame.f_code.co_name - - def funcnames(self, tb): - """ Extract function names from a traceback """ - names = [] - while tb: - names.append(self.funcname(tb.tb_frame)) - tb = tb.tb_next - return names - class ExceptionGroupConstructionTests(ExceptionGroupTestUtils): def test_construction_simple(self): # create a simple exception group and check that # it is constructed as expected bind = functools.partial - eg = self.create_EG( - [bind(self.raiseValueError, 1), - bind(self.raiseTypeError, int), - bind(self.raiseValueError, 2), - ], message='hello world') + eg = self.newEG( + [bind(self.newVE, 1), + bind(self.newTE, int), + bind(self.newVE, 2), + ], message='simple EG') self.assertEqual(len(eg.excs), 3) self.assertMatchesTemplate(eg, @@ -83,39 +73,39 @@ def test_construction_simple(self): self.assertEqual(list(eg), list(eg.excs)) # check message - self.assertEqual(eg.message, 'hello world') - self.assertEqual(eg.args, ('hello world',)) + self.assertEqual(eg.message, 'simple EG') + self.assertEqual(eg.args, ('simple EG',)) # check tracebacks for e in eg: expected = [ - 'create_EG', - 'create_EG', - 'raise'+type(e).__name__, + 'newEG', + 'newEG', + 'new'+ ''.join(filter(str.isupper, type(e).__name__)), ] etb = eg.extract_traceback(e) - self.assertEqual(expected, [self.funcname(f) for f in etb]) + self.assertEqual(expected, [f.name for f in etb]) def test_construction_nested(self): # create a nested exception group and check that # it is constructed as expected bind = functools.partial - level1 = lambda i: self.create_EG([ - bind(self.raiseValueError, i), - bind(self.raiseTypeError, int), - bind(self.raiseValueError, i+1), + level1 = lambda i: self.newEG([ + bind(self.newVE, i), + bind(self.newTE, int), + bind(self.newVE, i+1), ]) - def raiseException(e): raise e - level2 = lambda i : self.create_EG([ - bind(raiseException, level1(i)), - bind(raiseException, level1(i+1)), - bind(self.raiseValueError, i+2), + def raiseExc(e): raise e + level2 = lambda i : self.newEG([ + bind(raiseExc, level1(i)), + bind(raiseExc, level1(i+1)), + bind(self.newVE, i+2), ]) - level3 = lambda i : self.create_EG([ - bind(raiseException, level2(i+1)), - bind(self.raiseValueError, i+2), + level3 = lambda i : self.newEG([ + bind(raiseExc, level2(i+1)), + bind(self.newVE, i+2), ]) eg = level3(5) @@ -130,7 +120,6 @@ def raiseException(e): raise e ]) # check iteration - self.assertEqual(len(list(eg)), 8) # check tracebacks @@ -140,23 +129,96 @@ def raiseException(e): raise e all_excs = list(eg) for e in all_excs[0:6]: expected = [ - 'create_EG', - 'create_EG', - 'raiseException', - 'create_EG', - 'create_EG', - 'raiseException', - 'create_EG', - 'create_EG', - 'raise'+type(e).__name__, + 'newEG', + 'newEG', + 'raiseExc', + 'newEG', + 'newEG', + 'raiseExc', + 'newEG', + 'newEG', + 'new' + ''.join(filter(str.isupper, type(e).__name__)), ] etb = eg.extract_traceback(e) - self.assertEqual(expected, [self.funcname(f) for f in etb]) - self.assertEqual(['create_EG', 'create_EG', 'raiseException', - 'create_EG', 'create_EG', 'raiseValueError'], - [self.funcname(f) for f in eg.extract_traceback(all_excs[6])]) - self.assertEqual(['create_EG', 'create_EG', 'raiseValueError'], - [self.funcname(f) for f in eg.extract_traceback(all_excs[7])]) + self.assertEqual(expected, [f.name for f in etb]) + self.assertEqual([ + 'newEG', 'newEG', 'raiseExc', 'newEG', 'newEG', 'newVE'], + [f.name for f in eg.extract_traceback(all_excs[6])]) + self.assertEqual(['newEG', 'newEG', 'newVE'], + [f.name for f in eg.extract_traceback(all_excs[7])]) + + +class ExceptionGroupRenderTests(ExceptionGroupTestUtils): + def test_stack_summary_simple(self): + bind = functools.partial + eg = self.newEG( + [bind(self.newVE, 1), + bind(self.newTE, int), + bind(self.newVE, 2), + ], message='hello world') + + summary = StackGroupSummary.extract(eg) + self.assertEqual(4, len(summary)) + indents = [e[0] for e in summary] + exc_reprs = [e[1] for e in summary] + frames = [e[2] for e in summary] + self.assertEqual(indents, [0,4,4,4]) + self.assertEqual(exc_reprs, [repr(e) for e in [eg] + eg.excs]) + functions = [ + ['newEG'], + ['newEG', 'newVE'], + ['newEG', 'newTE'], + ['newEG', 'newVE'], + ] + for expected, found in zip (functions, frames): + self.assertEqual(len(expected), len(found)) + self.assertEqual(expected, [f.name for f in found]) + + def test_stack_summary_nested(self): + bind = functools.partial + level1 = lambda i: self.newEG([ + bind(self.newVE, i), + bind(self.newTE, int), + bind(self.newVE, i+1), + ]) + + def raiseExc(e): raise e + level2 = lambda i : self.newEG([ + bind(raiseExc, level1(i)), + bind(raiseExc, level1(i+1)), + bind(self.newVE, i+2), + ]) + + level3 = lambda i : self.newEG([ + bind(raiseExc, level2(i+1)), + bind(self.newVE, i+2), + ]) + eg = level3(5) + + summary = StackGroupSummary.extract(eg) + self.assertEqual(12, len(summary)) + indents = [e[0] for e in summary] + exc_reprs = [e[1] for e in summary] + frames = [e[2] for e in summary] + expected = [ + [0, eg, ['newEG']], + [4, eg.excs[0], ['newEG', 'raiseExc', 'newEG']], + [8, eg.excs[0].excs[0], ['newEG', 'raiseExc', 'newEG']], + [12, eg.excs[0].excs[0].excs[0], ['newEG', 'newVE']], + [12, eg.excs[0].excs[0].excs[1], ['newEG', 'newTE']], + [12, eg.excs[0].excs[0].excs[2], ['newEG', 'newVE']], + [8, eg.excs[0].excs[1], ['newEG', 'raiseExc', 'newEG']], + [12, eg.excs[0].excs[1].excs[0], ['newEG', 'newVE']], + [12, eg.excs[0].excs[1].excs[1], ['newEG', 'newTE']], + [12, eg.excs[0].excs[1].excs[2], ['newEG', 'newVE']], + [8, eg.excs[0].excs[2], ['newEG', 'newVE']], + [4, eg.excs[1], ['newEG', 'newVE']], + ] + self.assertEqual(indents, [e[0] for e in expected]) + self.assertEqual(exc_reprs, [repr(e[1]) for e in expected]) + expected_names = [e[2] for e in expected] + actual_names = [[f.name for f in frame] for frame in frames] + self.assertSequenceEqual(expected_names, actual_names) class ExceptionGroupSplitTests(ExceptionGroupTestUtils): @@ -164,7 +226,7 @@ class ExceptionGroupSplitTests(ExceptionGroupTestUtils): def _split_exception_group(self, eg, types): """ Split an EG and do some sanity checks on the result """ self.assertIsInstance(eg, ExceptionGroup) - fnames = self.funcnames(eg.__traceback__) + fnames = [t.name for t in traceback.extract_tb(eg.__traceback__)] all_excs = list(eg) match, rest = eg.split(types) @@ -184,8 +246,9 @@ def _split_exception_group(self, eg, types): for e in rest: self.assertNotIsInstance(e, types) - # check tracebacks for part in [match, rest]: + self.assertEqual(eg.message, part.message) + # check tracebacks for e in part: self.assertEqual( eg.extract_traceback(e), @@ -196,30 +259,31 @@ def _split_exception_group(self, eg, types): def test_split_nested(self): # create a nested exception group bind = functools.partial - level1 = lambda i: self.create_EG([ - bind(self.raiseValueError, i), - bind(self.raiseTypeError, int), - bind(self.raiseValueError, i+11), - ]) - - def raiseException(e): raise e - level2 = lambda i : self.create_EG([ - bind(raiseException, level1(i)), - bind(raiseException, level1(i+22)), - bind(self.raiseValueError, i+33), - ]) - - level3 = lambda i : self.create_EG([ - bind(raiseException, level2(i+44)), - bind(self.raiseValueError, i+55), - ]) + level1 = lambda i: self.newEG([ + bind(self.newVE, i), + bind(self.newTE, int), + bind(self.newVE, i+11), + ], message='level1') + + def raiseExc(e): raise e + level2 = lambda i : self.newEG([ + bind(raiseExc, level1(i)), + bind(raiseExc, level1(i+22)), + bind(self.newVE, i+33), + ], message='level2') + + level3 = lambda i : self.newEG([ + bind(raiseExc, level2(i+44)), + bind(self.newVE, i+55), + ], message='split me') try: raise level3(6) except ExceptionGroup as e: eg = e - fnames = ['test_split_nested', 'create_EG'] - self.assertEqual(self.funcnames(eg.__traceback__), fnames) + fnames = ['test_split_nested', 'newEG'] + self.assertEqual(fnames, + [t.name for t in traceback.extract_tb(eg.__traceback__)]) eg_template = [ [ @@ -271,51 +335,52 @@ class ExceptionGroupCatchTests(ExceptionGroupTestUtils): def setUp(self): super().setUp() - # create a nested exception group + # create a nested exception group bind = functools.partial - level1 = lambda i: self.create_EG([ - bind(self.raiseValueError, i), - bind(self.raiseTypeError, int), - bind(self.raiseValueError, i+10), + level1 = lambda i: self.newEG([ + bind(self.newVE, i), + bind(self.newTE, int), + bind(self.newVE, i+10), ]) - def raiseException(e): raise e - level2 = lambda i : self.create_EG([ - bind(raiseException, level1(i)), - bind(raiseException, level1(i+20)), - bind(self.raiseValueError, i+30), + def raiseExc(e): raise e + level2 = lambda i : self.newEG([ + bind(raiseExc, level1(i)), + bind(raiseExc, level1(i+20)), + bind(self.newVE, i+30), ]) - level3 = lambda i : self.create_EG([ - bind(raiseException, level2(i+40)), - bind(self.raiseValueError, i+50), - ]) + level3 = lambda i : self.newEG([ + bind(raiseExc, level2(i+40)), + bind(self.newVE, i+50), + ], message='nested EG') try: raise level3(5) except ExceptionGroup as e: self.eg = e - fnames = ['setUp', 'create_EG'] - self.assertEqual(self.funcnames(self.eg.__traceback__), fnames) + fnames = ['setUp', 'newEG'] + self.assertEqual(fnames, + [t.name for t in traceback.extract_tb(self.eg.__traceback__)]) # templates self.eg_template = [ - [ - [ValueError(45), TypeError(int), ValueError(55)], - [ValueError(65), TypeError(int), ValueError(75)], - ValueError(75), - ], - ValueError(55) - ] + [ + [ValueError(45), TypeError(int), ValueError(55)], + [ValueError(65), TypeError(int), ValueError(75)], + ValueError(75), + ], + ValueError(55) + ] self.valueErrors_template = [ - [ - [ValueError(45), ValueError(55)], - [ValueError(65), ValueError(75)], - ValueError(75), - ], - ValueError(55) - ] + [ + [ValueError(45), ValueError(55)], + [ValueError(65), ValueError(75)], + ValueError(75), + ], + ValueError(55) + ] self.typeErrors_template = [[[TypeError(int)],[TypeError(int)]]] @@ -323,7 +388,7 @@ def raiseException(e): raise e def checkMatch(self, exc, template, orig_eg): self.assertMatchesTemplate(exc, template) for e in exc: - f_data = lambda f: [f.f_code.co_name, f.f_lineno] + f_data = lambda f: [f.name, f.lineno] new = list(map(f_data, exc.extract_traceback(e))) if e in orig_eg: old = list(map(f_data, orig_eg.extract_traceback(e))) From 7b0b2b4ba9b62d8f0f196458936348684854f319 Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Wed, 4 Nov 2020 00:34:05 +0000 Subject: [PATCH 47/73] added test for format and render --- Lib/test/test_exception_group.py | 40 ++++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/Lib/test/test_exception_group.py b/Lib/test/test_exception_group.py index 087dbe0e1d646a..f6cf0e9c3e17e7 100644 --- a/Lib/test/test_exception_group.py +++ b/Lib/test/test_exception_group.py @@ -4,7 +4,8 @@ import traceback import unittest from exception_group import ExceptionGroup, TracebackGroup, StackGroupSummary - +from io import StringIO +import re class ExceptionGroupTestBase(unittest.TestCase): @@ -162,17 +163,33 @@ def test_stack_summary_simple(self): indents = [e[0] for e in summary] exc_reprs = [e[1] for e in summary] frames = [e[2] for e in summary] - self.assertEqual(indents, [0,4,4,4]) - self.assertEqual(exc_reprs, [repr(e) for e in [eg] + eg.excs]) - functions = [ - ['newEG'], - ['newEG', 'newVE'], - ['newEG', 'newTE'], - ['newEG', 'newVE'], + expected = [ + [0, eg, ['newEG']], + [4, eg.excs[0], ['newEG', 'newVE']], + [4, eg.excs[1], ['newEG', 'newTE']], + [4, eg.excs[2], ['newEG', 'newVE']], ] - for expected, found in zip (functions, frames): - self.assertEqual(len(expected), len(found)) - self.assertEqual(expected, [f.name for f in found]) + self.assertEqual(indents, [e[0] for e in expected]) + self.assertEqual(exc_reprs, [repr(e[1]) for e in expected]) + expected_names = [e[2] for e in expected] + actual_names = [[f.name for f in frame] for frame in frames] + self.assertSequenceEqual(expected_names, actual_names) + + self.check_format_and_render(eg, expected) + + def check_format_and_render(self, eg, expected): + re_arr = ['.*'] + for e in expected: + re_arr.append(type(e[1]).__name__) + for name in e[2]: + re_arr.append(name) + re_arr.append('.*') + format_re = re.compile('.*'.join(re_arr), re.MULTILINE | re.DOTALL) + format_output = ExceptionGroup.format(eg) + self.assertRegex(''.join(format_output), format_re) + render_output = StringIO() + ExceptionGroup.render(eg, file=render_output) + self.assertRegex(''.join(render_output.getvalue()), format_re) def test_stack_summary_nested(self): bind = functools.partial @@ -220,7 +237,6 @@ def raiseExc(e): raise e actual_names = [[f.name for f in frame] for frame in frames] self.assertSequenceEqual(expected_names, actual_names) - class ExceptionGroupSplitTests(ExceptionGroupTestUtils): def _split_exception_group(self, eg, types): From 100bd4605d452c0a19b82d51f0473a2bb3483fbe Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Wed, 4 Nov 2020 13:57:42 +0000 Subject: [PATCH 48/73] traceback.TracebackException is awesome --- Lib/exception_group.py | 20 +++---- Lib/test/test_exception_group.py | 93 ++++++++++++++------------------ 2 files changed, 48 insertions(+), 65 deletions(-) diff --git a/Lib/exception_group.py b/Lib/exception_group.py index 0067d4559cf445..a61b4021f652ca 100644 --- a/Lib/exception_group.py +++ b/Lib/exception_group.py @@ -125,11 +125,10 @@ def extract_traceback(self, exc): def format(exc, **kwargs): result = [] summary = StackGroupSummary.extract(exc, **kwargs) - for indent, exc_str, stack in summary: - prefix = ' '*indent - result.append(f"{prefix}{exc_str}:") - stack = traceback.StackSummary.format(stack) - result.extend([textwrap.indent(l.rstrip(), prefix) for l in stack]) + for indent, traceback_exception in summary: + stack = traceback_exception.format() + result.extend( + [textwrap.indent(l.rstrip(), ' '*indent) for l in stack]) return result @staticmethod @@ -160,19 +159,16 @@ def catch(types, handler): class StackGroupSummary(list): @classmethod - def extract(klass, exc, *, indent=0, result=None, - limit=None, lookup_lines=True, capture_locals=False): + def extract(klass, exc, *, indent=0, result=None, **kwargs): if result is None: result = klass() - result.append([ - indent, - f"{exc!r}", - traceback.extract_tb(exc.__traceback__, limit=limit)]) + te = traceback.TracebackException.from_exception(exc, **kwargs) + result.append([indent, te]) if isinstance(exc, ExceptionGroup): for e in exc.excs: StackGroupSummary.extract( - e, indent=indent+4, result=result, limit=limit) + e, indent=indent+4, result=result, **kwargs) return result class ExceptionGroupCatcher: diff --git a/Lib/test/test_exception_group.py b/Lib/test/test_exception_group.py index f6cf0e9c3e17e7..8e832cbfd23846 100644 --- a/Lib/test/test_exception_group.py +++ b/Lib/test/test_exception_group.py @@ -5,7 +5,6 @@ import unittest from exception_group import ExceptionGroup, TracebackGroup, StackGroupSummary from io import StringIO -import re class ExceptionGroupTestBase(unittest.TestCase): @@ -150,7 +149,7 @@ def raiseExc(e): raise e class ExceptionGroupRenderTests(ExceptionGroupTestUtils): - def test_stack_summary_simple(self): + def test_simple(self): bind = functools.partial eg = self.newEG( [bind(self.newVE, 1), @@ -158,38 +157,35 @@ def test_stack_summary_simple(self): bind(self.newVE, 2), ], message='hello world') - summary = StackGroupSummary.extract(eg) - self.assertEqual(4, len(summary)) - indents = [e[0] for e in summary] - exc_reprs = [e[1] for e in summary] - frames = [e[2] for e in summary] - expected = [ - [0, eg, ['newEG']], - [4, eg.excs[0], ['newEG', 'newVE']], - [4, eg.excs[1], ['newEG', 'newTE']], - [4, eg.excs[2], ['newEG', 'newVE']], + expected = [ # (indent, exception) pairs + (0, eg), + (4, eg.excs[0]), + (4, eg.excs[1]), + (4, eg.excs[2]), ] - self.assertEqual(indents, [e[0] for e in expected]) - self.assertEqual(exc_reprs, [repr(e[1]) for e in expected]) - expected_names = [e[2] for e in expected] - actual_names = [[f.name for f in frame] for frame in frames] - self.assertSequenceEqual(expected_names, actual_names) - - self.check_format_and_render(eg, expected) - - def check_format_and_render(self, eg, expected): - re_arr = ['.*'] - for e in expected: - re_arr.append(type(e[1]).__name__) - for name in e[2]: - re_arr.append(name) - re_arr.append('.*') - format_re = re.compile('.*'.join(re_arr), re.MULTILINE | re.DOTALL) + + self.check_summary_format_and_render(eg, expected) + + def check_summary_format_and_render(self, eg, expected): + makeTE = traceback.TracebackException.from_exception + + # StackGroupSummary.extract + summary = StackGroupSummary.extract(eg) + self.assertEqual(len(expected), len(summary)) + self.assertEqual([e[0] for e in summary], + [e[0] for e in expected]) + self.assertEqual([e[1] for e in summary], + [makeTE(e) for e in [e[1] for e in expected]]) + + # ExceptionGroup.format format_output = ExceptionGroup.format(eg) - self.assertRegex(''.join(format_output), format_re) render_output = StringIO() ExceptionGroup.render(eg, file=render_output) - self.assertRegex(''.join(render_output.getvalue()), format_re) + + self.assertIsInstance(format_output, list) + self.assertIsInstance(render_output.getvalue(), str) + self.assertEqual("".join(format_output).replace('\n',''), + render_output.getvalue().replace('\n','')) def test_stack_summary_nested(self): bind = functools.partial @@ -212,30 +208,21 @@ def raiseExc(e): raise e ]) eg = level3(5) - summary = StackGroupSummary.extract(eg) - self.assertEqual(12, len(summary)) - indents = [e[0] for e in summary] - exc_reprs = [e[1] for e in summary] - frames = [e[2] for e in summary] - expected = [ - [0, eg, ['newEG']], - [4, eg.excs[0], ['newEG', 'raiseExc', 'newEG']], - [8, eg.excs[0].excs[0], ['newEG', 'raiseExc', 'newEG']], - [12, eg.excs[0].excs[0].excs[0], ['newEG', 'newVE']], - [12, eg.excs[0].excs[0].excs[1], ['newEG', 'newTE']], - [12, eg.excs[0].excs[0].excs[2], ['newEG', 'newVE']], - [8, eg.excs[0].excs[1], ['newEG', 'raiseExc', 'newEG']], - [12, eg.excs[0].excs[1].excs[0], ['newEG', 'newVE']], - [12, eg.excs[0].excs[1].excs[1], ['newEG', 'newTE']], - [12, eg.excs[0].excs[1].excs[2], ['newEG', 'newVE']], - [8, eg.excs[0].excs[2], ['newEG', 'newVE']], - [4, eg.excs[1], ['newEG', 'newVE']], + expected = [ # (indent, exception) pairs + (0, eg), + (4, eg.excs[0]), + (8, eg.excs[0].excs[0]), + (12, eg.excs[0].excs[0].excs[0]), + (12, eg.excs[0].excs[0].excs[1]), + (12, eg.excs[0].excs[0].excs[2]), + (8, eg.excs[0].excs[1]), + (12, eg.excs[0].excs[1].excs[0]), + (12, eg.excs[0].excs[1].excs[1]), + (12, eg.excs[0].excs[1].excs[2]), + (8, eg.excs[0].excs[2]), + (4, eg.excs[1]), ] - self.assertEqual(indents, [e[0] for e in expected]) - self.assertEqual(exc_reprs, [repr(e[1]) for e in expected]) - expected_names = [e[2] for e in expected] - actual_names = [[f.name for f in frame] for frame in frames] - self.assertSequenceEqual(expected_names, actual_names) + self.check_summary_format_and_render(eg, expected) class ExceptionGroupSplitTests(ExceptionGroupTestUtils): From 1ddacced6a98d4f079b8750a3c34543bbab8e583 Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Thu, 5 Nov 2020 10:22:13 +0000 Subject: [PATCH 49/73] Delete tg1.py --- tg1.py | 37 ------------------------------------- 1 file changed, 37 deletions(-) delete mode 100644 tg1.py diff --git a/tg1.py b/tg1.py deleted file mode 100644 index 2e60eab62d7007..00000000000000 --- a/tg1.py +++ /dev/null @@ -1,37 +0,0 @@ -import asyncio -import exception_group - -async def t1(): - await asyncio.sleep(0.5) - 1 / 0 - -async def t2(): - async with asyncio.TaskGroup() as tg: - tg.create_task(t21()) - tg.create_task(t22()) - -async def t21(): - await asyncio.sleep(0.3) - raise ValueError - -async def t22(): - await asyncio.sleep(0.7) - raise TypeError - -async def main(): - async with asyncio.TaskGroup() as tg: - tg.create_task(t1()) - tg.create_task(t2()) - - -def run(*args): - try: - asyncio.run(*args) - except exception_group.ExceptionGroup as e: - print('============') - exception_group.ExceptionGroup.render(e) - print('^^^^^^^^^^^^') - raise - - -run(main()) From 1faf80a26028c8201ba24e5a3c38792ff796aae7 Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Thu, 5 Nov 2020 17:39:57 +0000 Subject: [PATCH 50/73] move extract_traceback to test script --- Lib/exception_group.py | 24 ------------------- Lib/test/test_exception_group.py | 40 +++++++++++++++++++++++++------- 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/Lib/exception_group.py b/Lib/exception_group.py index a61b4021f652ca..e833e5fb78bbca 100644 --- a/Lib/exception_group.py +++ b/Lib/exception_group.py @@ -97,30 +97,6 @@ def subgroup(self, keep): match, _ = self.project(lambda e: e in keep) return match - def extract_traceback(self, exc): - """ returns the traceback of a single exception - - If exc is in this exception group, return its - traceback as the concatenation of the outputs - of traceback.extract_tb() on each segment of - it traceback (one per each ExceptionGroup that - it belongs to). - """ - if exc not in self: - return None - e = self.subgroup([exc]) - result = None - while e is not None: - if isinstance(e, ExceptionGroup): - assert len(e.excs) == 1 and exc in e - r = traceback.extract_tb(e.__traceback__) - if result is not None: - result.extend(r) - else: - result = r - e = e.excs[0] if isinstance(e, ExceptionGroup) else None - return result - @staticmethod def format(exc, **kwargs): result = [] diff --git a/Lib/test/test_exception_group.py b/Lib/test/test_exception_group.py index 8e832cbfd23846..479c02755e3864 100644 --- a/Lib/test/test_exception_group.py +++ b/Lib/test/test_exception_group.py @@ -53,6 +53,30 @@ def newVE(self, v): def newTE(self, t): raise TypeError(t) + def extract_traceback(self, exc, eg): + """ returns the traceback of a single exception + + If exc is in the exception group, return its + traceback as the concatenation of the outputs + of traceback.extract_tb() on each segment of + it traceback (one per each ExceptionGroup that + it belongs to). + """ + if exc not in eg: + return None + e = eg.subgroup([exc]) + result = None + while e is not None: + if isinstance(e, ExceptionGroup): + assert len(e.excs) == 1 and exc in e + r = traceback.extract_tb(e.__traceback__) + if result is not None: + result.extend(r) + else: + result = r + e = e.excs[0] if isinstance(e, ExceptionGroup) else None + return result + class ExceptionGroupConstructionTests(ExceptionGroupTestUtils): def test_construction_simple(self): @@ -83,7 +107,7 @@ def test_construction_simple(self): 'newEG', 'new'+ ''.join(filter(str.isupper, type(e).__name__)), ] - etb = eg.extract_traceback(e) + etb = self.extract_traceback(e, eg) self.assertEqual(expected, [f.name for f in etb]) def test_construction_nested(self): @@ -139,13 +163,13 @@ def raiseExc(e): raise e 'newEG', 'new' + ''.join(filter(str.isupper, type(e).__name__)), ] - etb = eg.extract_traceback(e) + etb = self.extract_traceback(e, eg) self.assertEqual(expected, [f.name for f in etb]) self.assertEqual([ 'newEG', 'newEG', 'raiseExc', 'newEG', 'newEG', 'newVE'], - [f.name for f in eg.extract_traceback(all_excs[6])]) + [f.name for f in self.extract_traceback(all_excs[6], eg)]) self.assertEqual(['newEG', 'newEG', 'newVE'], - [f.name for f in eg.extract_traceback(all_excs[7])]) + [f.name for f in self.extract_traceback(all_excs[7], eg)]) class ExceptionGroupRenderTests(ExceptionGroupTestUtils): @@ -254,8 +278,8 @@ def _split_exception_group(self, eg, types): # check tracebacks for e in part: self.assertEqual( - eg.extract_traceback(e), - part.extract_traceback(e)) + self.extract_traceback(e, eg), + self.extract_traceback(e, part)) return match, rest @@ -392,9 +416,9 @@ def checkMatch(self, exc, template, orig_eg): self.assertMatchesTemplate(exc, template) for e in exc: f_data = lambda f: [f.name, f.lineno] - new = list(map(f_data, exc.extract_traceback(e))) + new = list(map(f_data, self.extract_traceback(e, exc))) if e in orig_eg: - old = list(map(f_data, orig_eg.extract_traceback(e))) + old = list(map(f_data, self.extract_traceback(e, orig_eg))) self.assertSequenceEqual(old, new[-len(old):]) class BaseHandler: From 025b4329807a3a972ae799617e4bea209df9312f Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Wed, 11 Nov 2020 02:14:26 +0000 Subject: [PATCH 51/73] pep8 formatting --- Lib/exception_group.py | 11 ++- Lib/test/test_exception_group.py | 160 ++++++++++++++++++------------- 2 files changed, 103 insertions(+), 68 deletions(-) diff --git a/Lib/exception_group.py b/Lib/exception_group.py index e833e5fb78bbca..2008ef045adc80 100644 --- a/Lib/exception_group.py +++ b/Lib/exception_group.py @@ -6,7 +6,7 @@ class TracebackGroup: def __init__(self, excs): - self.tb_next_map = {} # exception id to tb + self.tb_next_map = {} # exception id to tb for e in excs: if isinstance(e, ExceptionGroup): for e_ in e.excs: @@ -19,8 +19,8 @@ def __init__(self, excs): else: self.tb_next_map[id(e)] = e.__traceback__ -class ExceptionGroup(BaseException): +class ExceptionGroup(BaseException): def __init__(self, excs, message=None, *, tb=None): """ Construct a new ExceptionGroup @@ -54,7 +54,7 @@ def project(self, condition, with_complement=False): match = [] rest = [] if with_complement else None for e in self.excs: - if isinstance(e, ExceptionGroup): # recurse + if isinstance(e, ExceptionGroup): # recurse e_match, e_rest = e.project( condition, with_complement=with_complement) if not e_match.is_empty(): @@ -68,10 +68,12 @@ def project(self, condition, with_complement=False): rest.append(e) match_exc = ExceptionGroup(match, tb=self.__traceback__) + def copy_metadata(src, target): target.message = src.message target.__context__ = src.__context__ target.__cause__ = src.__cause__ + copy_metadata(self, match_exc) if with_complement: rest_exc = ExceptionGroup(rest, tb=self.__traceback__) @@ -133,6 +135,7 @@ def __repr__(self): def catch(types, handler): return ExceptionGroupCatcher(types, handler) + class StackGroupSummary(list): @classmethod def extract(klass, exc, *, indent=0, result=None, **kwargs): @@ -147,6 +150,7 @@ def extract(klass, exc, *, indent=0, result=None, **kwargs): e, indent=indent+4, result=result, **kwargs) return result + class ExceptionGroupCatcher: """ Based on trio.MultiErrorCatcher """ @@ -222,4 +226,3 @@ def __exit__(self, etype, exc, tb): _, value, _ = sys.exc_info() assert value is to_raise value.__context__ = old_context - diff --git a/Lib/test/test_exception_group.py b/Lib/test/test_exception_group.py index 479c02755e3864..602ea1cdec4474 100644 --- a/Lib/test/test_exception_group.py +++ b/Lib/test/test_exception_group.py @@ -6,15 +6,15 @@ from exception_group import ExceptionGroup, TracebackGroup, StackGroupSummary from io import StringIO -class ExceptionGroupTestBase(unittest.TestCase): +class ExceptionGroupTestBase(unittest.TestCase): def assertMatchesTemplate(self, exc, template): """ Assert that the exception matches the template """ if isinstance(exc, ExceptionGroup): self.assertIsInstance(template, collections.abc.Sequence) self.assertEqual(len(exc.excs), len(template)) for e, t in zip(exc.excs, template): - self.assertMatchesTemplate(e, t) + self.assertMatchesTemplate(e, t) else: self.assertIsInstance(template, BaseException) self.assertEqual(type(exc), type(template)) @@ -33,8 +33,8 @@ def tracebackGroupSanityCheck(self, exc): for e in exc.excs: self.tracebackGroupSanityCheck(e) -class ExceptionGroupTestUtils(ExceptionGroupTestBase): +class ExceptionGroupTestUtils(ExceptionGroupTestBase): def newEG(self, raisers, message=None): excs = [] for r in raisers: @@ -68,7 +68,7 @@ def extract_traceback(self, exc, eg): result = None while e is not None: if isinstance(e, ExceptionGroup): - assert len(e.excs) == 1 and exc in e + assert len(e.excs) == 1 and exc in e r = traceback.extract_tb(e.__traceback__) if result is not None: result.extend(r) @@ -77,8 +77,8 @@ def extract_traceback(self, exc, eg): e = e.excs[0] if isinstance(e, ExceptionGroup) else None return result -class ExceptionGroupConstructionTests(ExceptionGroupTestUtils): +class ExceptionGroupConstructionTests(ExceptionGroupTestUtils): def test_construction_simple(self): # create a simple exception group and check that # it is constructed as expected @@ -86,12 +86,12 @@ def test_construction_simple(self): eg = self.newEG( [bind(self.newVE, 1), bind(self.newTE, int), - bind(self.newVE, 2), - ], message='simple EG') + bind(self.newVE, 2), ], + message='simple EG') self.assertEqual(len(eg.excs), 3) - self.assertMatchesTemplate(eg, - [ValueError(1), TypeError(int), ValueError(2)]) + self.assertMatchesTemplate( + eg, [ValueError(1), TypeError(int), ValueError(2)]) # check iteration self.assertEqual(list(eg), list(eg.excs)) @@ -105,7 +105,7 @@ def test_construction_simple(self): expected = [ 'newEG', 'newEG', - 'new'+ ''.join(filter(str.isupper, type(e).__name__)), + 'new' + ''.join(filter(str.isupper, type(e).__name__)), ] etb = self.extract_traceback(e, eg) self.assertEqual(expected, [f.name for f in etb]) @@ -114,26 +114,34 @@ def test_construction_nested(self): # create a nested exception group and check that # it is constructed as expected bind = functools.partial - level1 = lambda i: self.newEG([ + + def level1(i): + return self.newEG([ bind(self.newVE, i), bind(self.newTE, int), bind(self.newVE, i+1), ]) - def raiseExc(e): raise e - level2 = lambda i : self.newEG([ + def raiseExc(e): + raise e + + def level2(i): + return self.newEG([ bind(raiseExc, level1(i)), bind(raiseExc, level1(i+1)), bind(self.newVE, i+2), ]) - level3 = lambda i : self.newEG([ + def level3(i): + return self.newEG([ bind(raiseExc, level2(i+1)), bind(self.newVE, i+2), ]) + eg = level3(5) - self.assertMatchesTemplate(eg, + self.assertMatchesTemplate( + eg, [ [ [ValueError(6), TypeError(int), ValueError(7)], @@ -165,11 +173,13 @@ def raiseExc(e): raise e ] etb = self.extract_traceback(e, eg) self.assertEqual(expected, [f.name for f in etb]) + + tb = self.extract_traceback(all_excs[6], eg) self.assertEqual([ 'newEG', 'newEG', 'raiseExc', 'newEG', 'newEG', 'newVE'], - [f.name for f in self.extract_traceback(all_excs[6], eg)]) - self.assertEqual(['newEG', 'newEG', 'newVE'], - [f.name for f in self.extract_traceback(all_excs[7], eg)]) + [f.name for f in tb]) + tb = self.extract_traceback(all_excs[7], eg) + self.assertEqual(['newEG', 'newEG', 'newVE'], [f.name for f in tb]) class ExceptionGroupRenderTests(ExceptionGroupTestUtils): @@ -178,8 +188,8 @@ def test_simple(self): eg = self.newEG( [bind(self.newVE, 1), bind(self.newTE, int), - bind(self.newVE, 2), - ], message='hello world') + bind(self.newVE, 2), ], + message='hello world') expected = [ # (indent, exception) pairs (0, eg), @@ -208,28 +218,35 @@ def check_summary_format_and_render(self, eg, expected): self.assertIsInstance(format_output, list) self.assertIsInstance(render_output.getvalue(), str) - self.assertEqual("".join(format_output).replace('\n',''), - render_output.getvalue().replace('\n','')) + self.assertEqual("".join(format_output).replace('\n', ''), + render_output.getvalue().replace('\n', '')) def test_stack_summary_nested(self): bind = functools.partial - level1 = lambda i: self.newEG([ + + def level1(i): + return self.newEG([ bind(self.newVE, i), bind(self.newTE, int), bind(self.newVE, i+1), ]) - def raiseExc(e): raise e - level2 = lambda i : self.newEG([ + def raiseExc(e): + raise e + + def level2(i): + return self.newEG([ bind(raiseExc, level1(i)), bind(raiseExc, level1(i+1)), bind(self.newVE, i+2), ]) - level3 = lambda i : self.newEG([ + def level3(i): + return self.newEG([ bind(raiseExc, level2(i+1)), bind(self.newVE, i+2), ]) + eg = level3(5) expected = [ # (indent, exception) pairs @@ -248,8 +265,8 @@ def raiseExc(e): raise e ] self.check_summary_format_and_render(eg, expected) -class ExceptionGroupSplitTests(ExceptionGroupTestUtils): +class ExceptionGroupSplitTests(ExceptionGroupTestUtils): def _split_exception_group(self, eg, types): """ Split an EG and do some sanity checks on the result """ self.assertIsInstance(eg, ExceptionGroup) @@ -260,7 +277,8 @@ def _split_exception_group(self, eg, types): self.assertIsInstance(match, ExceptionGroup) self.assertIsInstance(rest, ExceptionGroup) - self.assertEqual(len(list(all_excs)), len(list(match)) + len(list(rest))) + self.assertEqual(len(list(all_excs)), + len(list(match)) + len(list(rest))) for e in all_excs: self.assertIn(e, eg) @@ -286,31 +304,38 @@ def _split_exception_group(self, eg, types): def test_split_nested(self): # create a nested exception group bind = functools.partial - level1 = lambda i: self.newEG([ + + def level1(i): + return self.newEG([ bind(self.newVE, i), bind(self.newTE, int), bind(self.newVE, i+11), ], message='level1') - def raiseExc(e): raise e - level2 = lambda i : self.newEG([ + def raiseExc(e): + raise e + + def level2(i): + return self.newEG([ bind(raiseExc, level1(i)), bind(raiseExc, level1(i+22)), bind(self.newVE, i+33), ], message='level2') - level3 = lambda i : self.newEG([ + def level3(i): + return self.newEG([ bind(raiseExc, level2(i+44)), bind(self.newVE, i+55), ], message='split me') + try: raise level3(6) except ExceptionGroup as e: eg = e fnames = ['test_split_nested', 'newEG'] - self.assertEqual(fnames, - [t.name for t in traceback.extract_tb(eg.__traceback__)]) + tb = traceback.extract_tb(eg.__traceback__) + self.assertEqual(fnames, [t.name for t in tb]) eg_template = [ [ @@ -331,8 +356,7 @@ def raiseExc(e): raise e ValueError(61) ] - typeErrors_template = [[[TypeError(int)],[TypeError(int)]]] - + typeErrors_template = [[[TypeError(int)], [TypeError(int)]]] # Match Nothing match, rest = self._split_exception_group(eg, SyntaxError) @@ -364,31 +388,38 @@ def setUp(self): # create a nested exception group bind = functools.partial - level1 = lambda i: self.newEG([ + + def level1(i): + return self.newEG([ bind(self.newVE, i), bind(self.newTE, int), bind(self.newVE, i+10), ]) - def raiseExc(e): raise e - level2 = lambda i : self.newEG([ + def raiseExc(e): + raise e + + def level2(i): + return self.newEG([ bind(raiseExc, level1(i)), bind(raiseExc, level1(i+20)), bind(self.newVE, i+30), ]) - level3 = lambda i : self.newEG([ + def level3(i): + return self.newEG([ bind(raiseExc, level2(i+40)), bind(self.newVE, i+50), ], message='nested EG') + try: raise level3(5) except ExceptionGroup as e: self.eg = e fnames = ['setUp', 'newEG'] - self.assertEqual(fnames, - [t.name for t in traceback.extract_tb(self.eg.__traceback__)]) + tb = traceback.extract_tb(self.eg.__traceback__) + self.assertEqual(fnames, [t.name for t in tb]) # templates self.eg_template = [ @@ -409,13 +440,15 @@ def raiseExc(e): raise e ValueError(55) ] - self.typeErrors_template = [[[TypeError(int)],[TypeError(int)]]] - + self.typeErrors_template = [[[TypeError(int)], [TypeError(int)]]] def checkMatch(self, exc, template, orig_eg): self.assertMatchesTemplate(exc, template) for e in exc: - f_data = lambda f: [f.name, f.lineno] + + def f_data(f): + return [f.name, f.lineno] + new = list(map(f_data, self.extract_traceback(e, exc))) if e in orig_eg: old = list(map(f_data, self.extract_traceback(e, orig_eg))) @@ -449,23 +482,23 @@ class Handler(self.BaseHandler): def handle(self, eg): pass - ######### Catch nothing: + # ######## Catch nothing: caught, raised = self.apply_catcher(SyntaxError, Handler, eg) self.checkMatch(raised, eg_template, eg) self.assertIsNone(caught) - ######### Catch everything: + # ######## Catch everything: error_types = (ValueError, TypeError) caught, raised = self.apply_catcher(error_types, Handler, eg) self.assertIsNone(raised) self.checkMatch(caught, eg_template, eg) - ######### Catch TypeErrors: + # ######## Catch TypeErrors: caught, raised = self.apply_catcher(TypeError, Handler, eg) self.checkMatch(raised, valueErrors_template, eg) self.checkMatch(caught, typeErrors_template, eg) - ######### Catch ValueErrors: + # ######## Catch ValueErrors: error_types = (ValueError, SyntaxError) caught, raised = self.apply_catcher(error_types, Handler, eg) self.checkMatch(raised, typeErrors_template, eg) @@ -488,28 +521,27 @@ def handle(self, eg): newErrors_template = [ ValueError('foo'), [SyntaxError('bar'), ValueError('baz')]] - ######### Catch nothing: + # ######## Catch nothing: caught, raised = self.apply_catcher(SyntaxError, Handler, eg) self.checkMatch(raised, eg_template, eg) self.assertIsNone(caught) - ######### Catch everything: + # ######## Catch everything: error_types = (ValueError, TypeError) caught, raised = self.apply_catcher(error_types, Handler, eg) self.checkMatch(raised, newErrors_template, eg) self.checkMatch(caught, eg_template, eg) - ######### Catch TypeErrors: + # ######## Catch TypeErrors: caught, raised = self.apply_catcher(TypeError, Handler, eg) self.checkMatch(raised, [valueErrors_template, newErrors_template], eg) self.checkMatch(caught, typeErrors_template, eg) - ######### Catch ValueErrors: + # ######## Catch ValueErrors: caught, raised = self.apply_catcher((ValueError, OSError), Handler, eg) self.checkMatch(raised, [typeErrors_template, newErrors_template], eg) self.checkMatch(caught, valueErrors_template, eg) - def test_catch_handler_reraise_all_matched(self): eg = self.eg eg_template = self.eg_template @@ -526,24 +558,24 @@ def handle(self, eg): raise eg for handler in [Handler1, Handler2]: - ######### Catch nothing: + # ######## Catch nothing: caught, raised = self.apply_catcher(SyntaxError, handler, eg) # handler is never called self.checkMatch(raised, eg_template, eg) self.assertIsNone(caught) - ######### Catch everything: + # ######## Catch everything: error_types = (ValueError, TypeError) caught, raised = self.apply_catcher(error_types, handler, eg) self.checkMatch(raised, eg_template, eg) self.checkMatch(caught, eg_template, eg) - ######### Catch TypeErrors: + # ######## Catch TypeErrors: caught, raised = self.apply_catcher(TypeError, handler, eg) self.checkMatch(raised, eg_template, eg) self.checkMatch(caught, typeErrors_template, eg) - ######### Catch ValueErrors: + # ######## Catch ValueErrors: catch = (ValueError, SyntaxError) caught, raised = self.apply_catcher(catch, handler, eg) self.checkMatch(raised, eg_template, eg) @@ -566,12 +598,12 @@ def handle(self, eg): newErrors_template = [ ValueError('foo'), [SyntaxError('bar'), ValueError('baz')]] - ######### Catch TypeErrors: + # ######## Catch TypeErrors: caught, raised = self.apply_catcher(TypeError, Handler, eg) self.checkMatch(raised, [eg_template, newErrors_template], eg) self.checkMatch(caught, typeErrors_template, eg) - ######### Catch ValueErrors: + # ######## Catch ValueErrors: caught, raised = self.apply_catcher(ValueError, Handler, eg) self.checkMatch(raised, [eg_template, newErrors_template], eg) self.checkMatch(caught, valueErrors_template, eg) @@ -586,19 +618,19 @@ class Handler(self.BaseHandler): def handle(self, eg): raise ExceptionGroup( [eg.excs[0], - ValueError('foo'), - ExceptionGroup( + ValueError('foo'), + ExceptionGroup( [SyntaxError('bar'), ValueError('baz')])]) newErrors_template = [ ValueError('foo'), [SyntaxError('bar'), ValueError('baz')]] - ######### Catch TypeErrors: + # ######## Catch TypeErrors: caught, raised = self.apply_catcher(TypeError, Handler, eg) self.checkMatch(raised, [eg_template, newErrors_template], eg) self.checkMatch(caught, typeErrors_template, eg) - ######### Catch ValueErrors: + # ######## Catch ValueErrors: caught, raised = self.apply_catcher(ValueError, Handler, eg) # eg.excs[0] is reraised and eg.excs[1] is consumed self.checkMatch(raised, [[eg_template[0]], newErrors_template], eg) From 2fd59e7780bdd3e556e2da29dfb5fa8f9ab12a84 Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Wed, 11 Nov 2020 15:55:19 +0000 Subject: [PATCH 52/73] reduce repetition in tests --- Lib/test/test_exception_group.py | 192 ++++++++++--------------------- 1 file changed, 58 insertions(+), 134 deletions(-) diff --git a/Lib/test/test_exception_group.py b/Lib/test/test_exception_group.py index 602ea1cdec4474..1e624da3844edb 100644 --- a/Lib/test/test_exception_group.py +++ b/Lib/test/test_exception_group.py @@ -53,6 +53,42 @@ def newVE(self, v): def newTE(self, t): raise TypeError(t) + def newSimpleEG(self, message=None): + bind = functools.partial + return self.newEG( + [bind(self.newVE, 1), + bind(self.newTE, int), + bind(self.newVE, 2), ], + message=message) + + def newNestedEG(self, arg, message=None): + bind = functools.partial + + def level1(i): + return self.newEG([ + bind(self.newVE, i), + bind(self.newTE, int), + bind(self.newVE, i+1), + ]) + + def raiseExc(e): + raise e + + def level2(i): + return self.newEG([ + bind(raiseExc, level1(i)), + bind(raiseExc, level1(i+1)), + bind(self.newVE, i+2), + ]) + + def level3(i): + return self.newEG([ + bind(raiseExc, level2(i+1)), + bind(self.newVE, i+2), + ]) + + return level3(arg) + def extract_traceback(self, exc, eg): """ returns the traceback of a single exception @@ -83,11 +119,7 @@ def test_construction_simple(self): # create a simple exception group and check that # it is constructed as expected bind = functools.partial - eg = self.newEG( - [bind(self.newVE, 1), - bind(self.newTE, int), - bind(self.newVE, 2), ], - message='simple EG') + eg = self.newSimpleEG('simple EG') self.assertEqual(len(eg.excs), 3) self.assertMatchesTemplate( @@ -111,34 +143,7 @@ def test_construction_simple(self): self.assertEqual(expected, [f.name for f in etb]) def test_construction_nested(self): - # create a nested exception group and check that - # it is constructed as expected - bind = functools.partial - - def level1(i): - return self.newEG([ - bind(self.newVE, i), - bind(self.newTE, int), - bind(self.newVE, i+1), - ]) - - def raiseExc(e): - raise e - - def level2(i): - return self.newEG([ - bind(raiseExc, level1(i)), - bind(raiseExc, level1(i+1)), - bind(self.newVE, i+2), - ]) - - def level3(i): - return self.newEG([ - bind(raiseExc, level2(i+1)), - bind(self.newVE, i+2), - ]) - - eg = level3(5) + eg = self.newNestedEG(5) self.assertMatchesTemplate( eg, @@ -185,11 +190,7 @@ def level3(i): class ExceptionGroupRenderTests(ExceptionGroupTestUtils): def test_simple(self): bind = functools.partial - eg = self.newEG( - [bind(self.newVE, 1), - bind(self.newTE, int), - bind(self.newVE, 2), ], - message='hello world') + eg = self.newSimpleEG('hello world') expected = [ # (indent, exception) pairs (0, eg), @@ -222,32 +223,7 @@ def check_summary_format_and_render(self, eg, expected): render_output.getvalue().replace('\n', '')) def test_stack_summary_nested(self): - bind = functools.partial - - def level1(i): - return self.newEG([ - bind(self.newVE, i), - bind(self.newTE, int), - bind(self.newVE, i+1), - ]) - - def raiseExc(e): - raise e - - def level2(i): - return self.newEG([ - bind(raiseExc, level1(i)), - bind(raiseExc, level1(i+1)), - bind(self.newVE, i+2), - ]) - - def level3(i): - return self.newEG([ - bind(raiseExc, level2(i+1)), - bind(self.newVE, i+2), - ]) - - eg = level3(5) + eg = self.newNestedEG(15) expected = [ # (indent, exception) pairs (0, eg), @@ -302,34 +278,8 @@ def _split_exception_group(self, eg, types): return match, rest def test_split_nested(self): - # create a nested exception group - bind = functools.partial - - def level1(i): - return self.newEG([ - bind(self.newVE, i), - bind(self.newTE, int), - bind(self.newVE, i+11), - ], message='level1') - - def raiseExc(e): - raise e - - def level2(i): - return self.newEG([ - bind(raiseExc, level1(i)), - bind(raiseExc, level1(i+22)), - bind(self.newVE, i+33), - ], message='level2') - - def level3(i): - return self.newEG([ - bind(raiseExc, level2(i+44)), - bind(self.newVE, i+55), - ], message='split me') - try: - raise level3(6) + raise self.newNestedEG(25) except ExceptionGroup as e: eg = e @@ -339,21 +289,21 @@ def level3(i): eg_template = [ [ - [ValueError(50), TypeError(int), ValueError(61)], - [ValueError(72), TypeError(int), ValueError(83)], - ValueError(83), + [ValueError(26), TypeError(int), ValueError(27)], + [ValueError(27), TypeError(int), ValueError(28)], + ValueError(28), ], - ValueError(61) + ValueError(27) ] self.assertMatchesTemplate(eg, eg_template) valueErrors_template = [ [ - [ValueError(50), ValueError(61)], - [ValueError(72), ValueError(83)], - ValueError(83), + [ValueError(26), ValueError(27)], + [ValueError(27), ValueError(28)], + ValueError(28), ], - ValueError(61) + ValueError(27) ] typeErrors_template = [[[TypeError(int)], [TypeError(int)]]] @@ -386,34 +336,8 @@ class ExceptionGroupCatchTests(ExceptionGroupTestUtils): def setUp(self): super().setUp() - # create a nested exception group - bind = functools.partial - - def level1(i): - return self.newEG([ - bind(self.newVE, i), - bind(self.newTE, int), - bind(self.newVE, i+10), - ]) - - def raiseExc(e): - raise e - - def level2(i): - return self.newEG([ - bind(raiseExc, level1(i)), - bind(raiseExc, level1(i+20)), - bind(self.newVE, i+30), - ]) - - def level3(i): - return self.newEG([ - bind(raiseExc, level2(i+40)), - bind(self.newVE, i+50), - ], message='nested EG') - try: - raise level3(5) + raise self.newNestedEG(35) except ExceptionGroup as e: self.eg = e @@ -424,20 +348,20 @@ def level3(i): # templates self.eg_template = [ [ - [ValueError(45), TypeError(int), ValueError(55)], - [ValueError(65), TypeError(int), ValueError(75)], - ValueError(75), + [ValueError(36), TypeError(int), ValueError(37)], + [ValueError(37), TypeError(int), ValueError(38)], + ValueError(38), ], - ValueError(55) + ValueError(37) ] self.valueErrors_template = [ [ - [ValueError(45), ValueError(55)], - [ValueError(65), ValueError(75)], - ValueError(75), + [ValueError(36), ValueError(37)], + [ValueError(37), ValueError(38)], + ValueError(38), ], - ValueError(55) + ValueError(37) ] self.typeErrors_template = [[[TypeError(int)], [TypeError(int)]]] From 0d57f35c2d42ef9d8d93926fcf426a8d0e716ed4 Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Fri, 20 Nov 2020 02:13:59 +0000 Subject: [PATCH 53/73] move ExceptionGroup formatting to traceback.py --- Lib/exception_group.py | 19 +-- Lib/test/test_exception_group.py | 6 +- Lib/test/test_traceback.py | 210 +++++++++++++++++++++++++++++++ Lib/traceback.py | 90 ++++++++++++- 4 files changed, 303 insertions(+), 22 deletions(-) diff --git a/Lib/exception_group.py b/Lib/exception_group.py index 2008ef045adc80..60dcced05f0cb2 100644 --- a/Lib/exception_group.py +++ b/Lib/exception_group.py @@ -100,21 +100,10 @@ def subgroup(self, keep): return match @staticmethod - def format(exc, **kwargs): - result = [] - summary = StackGroupSummary.extract(exc, **kwargs) - for indent, traceback_exception in summary: - stack = traceback_exception.format() - result.extend( - [textwrap.indent(l.rstrip(), ' '*indent) for l in stack]) - return result - - @staticmethod - def render(exc, file=None, **kwargs): - if file is None: - file = sys.stderr - for line in ExceptionGroup.format(exc, **kwargs): - print(line, file=file) + def render(exc, limit=None, file=None, chain=True): + traceback.print_exception( + type(exc), exc, exc.__traceback__, + limit=limit, file=file, chain=chain) def __iter__(self): ''' iterate over the individual exceptions (flattens the tree) ''' diff --git a/Lib/test/test_exception_group.py b/Lib/test/test_exception_group.py index 1e624da3844edb..ccaf6c7185487c 100644 --- a/Lib/test/test_exception_group.py +++ b/Lib/test/test_exception_group.py @@ -3,7 +3,7 @@ import functools import traceback import unittest -from exception_group import ExceptionGroup, TracebackGroup, StackGroupSummary +from exception_group import ExceptionGroup, TracebackGroup from io import StringIO @@ -205,7 +205,7 @@ def check_summary_format_and_render(self, eg, expected): makeTE = traceback.TracebackException.from_exception # StackGroupSummary.extract - summary = StackGroupSummary.extract(eg) + summary = traceback.TracebackExceptionGroup.from_exception(eg).summary self.assertEqual(len(expected), len(summary)) self.assertEqual([e[0] for e in summary], [e[0] for e in expected]) @@ -213,7 +213,7 @@ def check_summary_format_and_render(self, eg, expected): [makeTE(e) for e in [e[1] for e in expected]]) # ExceptionGroup.format - format_output = ExceptionGroup.format(eg) + format_output = list(traceback.TracebackExceptionGroup.from_exception(eg).format()) render_output = StringIO() ExceptionGroup.render(eg, file=render_output) diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index 5df701caf0f01e..6687f0e1219653 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -12,6 +12,7 @@ from test.support.script_helper import assert_python_ok import textwrap +import exception_group import traceback @@ -1266,6 +1267,215 @@ def test_traceback_header(self): self.assertEqual(list(exc.format()), ["Exception: haven\n"]) +class TestTracebackGroupException(unittest.TestCase): + + def test_simple_exception(self): + def foo(): + 1/0 + try: + foo() + except Exception: + exc_info = sys.exc_info() + teg = traceback.TracebackExceptionGroup(*exc_info) + exc = traceback.TracebackException(*exc_info) + self.assertEqual(teg.summary, ((0, exc),)) + + try: + foo() + except Exception as e: + teg, exc = ( + traceback.TracebackExceptionGroup.from_exception( + e, limit=1, lookup_lines=False, capture_locals=True), + traceback.TracebackException.from_exception( + e, limit=1, lookup_lines=False, capture_locals=True)) + self.assertEqual(teg.summary, ((0, exc),)) + + def test_exception_group(self): + def f(): + 1/0 + + def g(v): + raise ValueError(v) + + try: + try: + try: + f() + except Exception as e: + exc1 = e + try: + g(42) + except Exception as e: + exc2 = e + raise exception_group.ExceptionGroup( + [exc1, exc2], message="eg1") + except exception_group.ExceptionGroup as e: + exc3 = e + try: + g(24) + except Exception as e: + exc4 = e + raise exception_group.ExceptionGroup( + [exc3, exc4], message="eg2") + except exception_group.ExceptionGroup as eg: + exc_info = sys.exc_info() + exc = traceback.TracebackExceptionGroup(*exc_info) + expected_stack = traceback.StackSummary.extract( + traceback.walk_tb(exc_info[2])) + expected_summary = ( + (0, traceback.TracebackException(*exc_info)), + (4, traceback.TracebackException.from_exception(exc3)), + (8, traceback.TracebackException.from_exception(exc1)), + (8, traceback.TracebackException.from_exception(exc2)), + (4, traceback.TracebackException.from_exception(exc4)), + ) + self.assertEqual(exc.summary, expected_summary) + formatted_exception_only = list(exc.format_exception_only()) + + expected_exception_only = [ + 'exception_group.ExceptionGroup: eg2\n', + ' exception_group.ExceptionGroup: eg1\n', + ' ZeroDivisionError: division by zero\n', + ' ValueError: 42\n', + ' ValueError: 24\n' + ] + + self.assertEqual(formatted_exception_only, expected_exception_only) + + formatted = ''.join(exc.format()).split('\n') + lineno_f = f.__code__.co_firstlineno + lineno_g = g.__code__.co_firstlineno + + expected = [ + 'Traceback (most recent call last):', + f' File "{__file__}", ' + f'line {lineno_g+21}, in test_exception_group', + ' raise exception_group.ExceptionGroup(', + 'exception_group.ExceptionGroup: eg2', + ' --------------------------------------------------------', + ' Traceback (most recent call last):', + f' File "{__file__}", ' + f'line {lineno_g+13}, in test_exception_group', + ' raise exception_group.ExceptionGroup(', + ' exception_group.ExceptionGroup: eg1', + ' ----------------------------------------------------', + ' Traceback (most recent call last):', + ' File ' + f'"{__file__}", line {lineno_g+6}, in ' + 'test_exception_group', + ' f()', + ' File ' + f'"{__file__}", line {lineno_f+1}, in ' + 'f', + ' 1/0', + ' ZeroDivisionError: division by zero', + ' ----------------------------------------------------', + ' Traceback (most recent call last):', + ' File ' + f'"{__file__}", line {lineno_g+10}, in ' + 'test_exception_group', + ' g(42)', + ' File ' + f'"{__file__}", line {lineno_g+1}, in ' + 'g', + ' raise ValueError(v)', + ' ValueError: 42', + ' --------------------------------------------------------', + ' Traceback (most recent call last):', + f' File "{__file__}", ' + f'line {lineno_g+18}, in test_exception_group', + ' g(24)', + f' File "{__file__}", ' + f'line {lineno_g+1}, in g', + ' raise ValueError(v)', + ' ValueError: 24', + ''] + + self.assertEqual(formatted, expected) + + def test_comparison(self): + try: + 1/0 + except Exception: + exc_info = sys.exc_info() + exc = traceback.TracebackExceptionGroup(*exc_info) + exc2 = traceback.TracebackExceptionGroup(*exc_info) + self.assertIsNot(exc, exc2) + self.assertEqual(exc, exc2) + self.assertNotEqual(exc, object()) + self.assertEqual(exc, ALWAYS_EQ) + + def test_unhashable(self): + class UnhashableException(Exception): + def __eq__(self, other): + return True + + ex1 = UnhashableException('ex1') + ex2 = UnhashableException('ex2') + try: + raise ex2 from ex1 + except UnhashableException: + try: + raise ex1 + except UnhashableException: + exc_info = sys.exc_info() + exc = traceback.TracebackExceptionGroup(*exc_info) + formatted = list(exc.format()) + self.assertIn('UnhashableException: ex2\n', formatted[2]) + self.assertIn('UnhashableException: ex1\n', formatted[6]) + + def test_limit(self): + def recurse(n): + if n: + recurse(n-1) + else: + 1/0 + try: + recurse(10) + except Exception: + exc_info = sys.exc_info() + exc = traceback.TracebackExceptionGroup(*exc_info, limit=5) + expected_stack = traceback.StackSummary.extract( + traceback.walk_tb(exc_info[2]), limit=5) + self.assertEqual(expected_stack, exc.summary[0][1].stack) + + def test_lookup_lines(self): + linecache.clearcache() + e = Exception("uh oh") + c = test_code('/foo.py', 'method') + f = test_frame(c, None, None) + tb = test_tb(f, 6, None) + exc = traceback.TracebackExceptionGroup(Exception, e, tb, lookup_lines=False) + self.assertEqual({}, linecache.cache) + linecache.updatecache('/foo.py', globals()) + self.assertEqual(exc.summary[0][1].stack[0].line, "import sys") + + def test_locals(self): + linecache.updatecache('/foo.py', globals()) + e = Exception("uh oh") + c = test_code('/foo.py', 'method') + f = test_frame(c, globals(), {'something': 1, 'other': 'string'}) + tb = test_tb(f, 6, None) + exc = traceback.TracebackExceptionGroup( + Exception, e, tb, capture_locals=True) + self.assertEqual( + exc.summary[0][1].stack[0].locals, {'something': '1', 'other': "'string'"}) + + def test_no_locals(self): + linecache.updatecache('/foo.py', globals()) + e = Exception("uh oh") + c = test_code('/foo.py', 'method') + f = test_frame(c, globals(), {'something': 1}) + tb = test_tb(f, 6, None) + exc = traceback.TracebackExceptionGroup(Exception, e, tb) + self.assertEqual(exc.summary[0][1].stack[0].locals, None) + + def test_traceback_header(self): + # do not print a traceback header if exc_traceback is None + # see issue #24695 + exc = traceback.TracebackExceptionGroup(Exception, Exception("haven"), None) + self.assertEqual(list(exc.format()), ["Exception: haven\n"]) + class MiscTest(unittest.TestCase): def test_all(self): diff --git a/Lib/traceback.py b/Lib/traceback.py index 457d92511af051..cf41610c1ed70c 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -1,16 +1,18 @@ """Extract, format and print information about Python stack traces.""" import collections +import exception_group import itertools import linecache import sys +import textwrap __all__ = ['extract_stack', 'extract_tb', 'format_exception', 'format_exception_only', 'format_list', 'format_stack', 'format_tb', 'print_exc', 'format_exc', 'print_exception', 'print_last', 'print_stack', 'print_tb', 'clear_frames', 'FrameSummary', 'StackSummary', 'TracebackException', - 'walk_stack', 'walk_tb'] + 'TracebackExceptionGroup', 'walk_stack', 'walk_tb'] # # Formatting and printing lists of traceback lines. @@ -110,7 +112,7 @@ def print_exception(exc, /, value=_sentinel, tb=_sentinel, limit=None, \ value, tb = _parse_value_tb(exc, value, tb) if file is None: file = sys.stderr - for line in TracebackException( + for line in TracebackExceptionGroup( type(value), value, tb, limit=limit).format(chain=chain): print(line, file=file, end="") @@ -126,7 +128,7 @@ def format_exception(exc, /, value=_sentinel, tb=_sentinel, limit=None, \ printed as does print_exception(). """ value, tb = _parse_value_tb(exc, value, tb) - return list(TracebackException( + return list(TracebackExceptionGroup( type(value), value, tb, limit=limit).format(chain=chain)) @@ -146,7 +148,7 @@ def format_exception_only(exc, /, value=_sentinel): """ if value is _sentinel: value = exc - return list(TracebackException( + return list(TracebackExceptionGroup( type(value), value, None).format_exception_only()) @@ -630,3 +632,83 @@ def format(self, *, chain=True): yield 'Traceback (most recent call last):\n' yield from self.stack.format() yield from self.format_exception_only() + + +class TracebackExceptionGroup: + """An exception or exception group ready for rendering. + + The traceback module captures enough attributes from the original exception + group to this intermediary form to ensure that no references are held, while + still being able to fully print or format it. + + Use `from_exception` to create TracebackExceptionGroup instances from exception + objects, or the constructor to create TracebackExceptionGroup instances from + individual components. + + - :attr:`summary` A tuple of (indentation level, TracebackException) pairs + """ + + def __init__(self, exc_type, exc_value, exc_traceback, **kwargs): + self.summary = tuple( + self._gen_summary( + exc_type, exc_value, exc_traceback, **kwargs)) + self._str = _some_str(exc_value) + + def _gen_summary(self, exc_type, exc_value, exc_traceback, + indent=0, **kwargs): + # recursively extract the sequence of (indent, TracebackException) + # pairs. If exc_value is a single exception, there is only one pair + # (with indent 0). If it is an exception group, we get one pair + # for each exception in it, showing the whole tree + te = TracebackException( + exc_type, exc_value, exc_traceback, **kwargs) + yield tuple((indent, te)) + if isinstance(exc_value, exception_group.ExceptionGroup): + for e in exc_value.excs: + yield from self._gen_summary( + type(e), e, e.__traceback__, indent=indent+4, **kwargs) + + @classmethod + def from_exception(cls, exc, *args, **kwargs): + """Create a TracebackException from an exception.""" + return cls(type(exc), exc, exc.__traceback__, *args, **kwargs) + + def format(self, *, chain=True): + """Format the exception or exception group. + + For an exception group, the shared part of the traceback is printed, + followed by a printout of each exception in the group, which is + expanded recursively. + + If chain is not *True*, *__cause__* and *__context__* will not be formatted. + + The return value is a generator of strings, each ending in a newline and + some containing internal newlines. `print_exception_group` is a wrapper + around this method which just prints the lines to a file. + """ + # TODO: should we add an arg to bound the number of exceptions printed? + # or should we use limit somehow for that? + for indent, te in self.summary: + if indent == 0: + yield from te.format(chain=chain) + else: + indent_str = ' '*indent + yield indent_str + '-'*(60-indent) +'\n' + yield textwrap.indent( + ''.join(list(te.format(chain=chain))), indent_str) + + def format_exception_only(self): + for indent, te in self.summary: + if indent == 0: + yield from te.format_exception_only() + else: + yield textwrap.indent( + ''.join(list(te.format_exception_only())),' '*indent) + + def __eq__(self, other): + if isinstance(other, TracebackExceptionGroup): + return self.__dict__ == other.__dict__ + return NotImplemented + + def __str__(self): + return self._str From 4cae22a1695f16d218714ce37d809a4fd0bcd2f6 Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Sun, 22 Nov 2020 21:40:50 +0000 Subject: [PATCH 54/73] Update Lib/traceback.py Co-authored-by: Guido van Rossum --- Lib/traceback.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/traceback.py b/Lib/traceback.py index cf41610c1ed70c..06c31f098e4841 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -637,7 +637,7 @@ def format(self, *, chain=True): class TracebackExceptionGroup: """An exception or exception group ready for rendering. - The traceback module captures enough attributes from the original exception + We capture enough attributes from the original exception group to this intermediary form to ensure that no references are held, while still being able to fully print or format it. From 8d6a2a9dadd6bd9a341e3f6799378abb78fccf7c Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Sun, 22 Nov 2020 21:41:08 +0000 Subject: [PATCH 55/73] Update Lib/traceback.py Co-authored-by: Guido van Rossum --- Lib/traceback.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/traceback.py b/Lib/traceback.py index 06c31f098e4841..0cd15439a78e4e 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -641,7 +641,7 @@ class TracebackExceptionGroup: group to this intermediary form to ensure that no references are held, while still being able to fully print or format it. - Use `from_exception` to create TracebackExceptionGroup instances from exception + Use `from_exception()` to create TracebackExceptionGroup instances from exception objects, or the constructor to create TracebackExceptionGroup instances from individual components. From f593579da26befbe5ba32d79590b25ca3ffc4c3a Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Sun, 22 Nov 2020 21:42:24 +0000 Subject: [PATCH 56/73] Update Lib/traceback.py Co-authored-by: Guido van Rossum --- Lib/traceback.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/traceback.py b/Lib/traceback.py index 0cd15439a78e4e..1a8fdd6e203bc0 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -670,7 +670,7 @@ def _gen_summary(self, exc_type, exc_value, exc_traceback, @classmethod def from_exception(cls, exc, *args, **kwargs): - """Create a TracebackException from an exception.""" + """Create a TracebackExceptionGroup from an exception.""" return cls(type(exc), exc, exc.__traceback__, *args, **kwargs) def format(self, *, chain=True): From 0276874df60f5df58f8149fee9b2a363dac1d95b Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Sun, 22 Nov 2020 21:42:56 +0000 Subject: [PATCH 57/73] Update Lib/traceback.py Co-authored-by: Guido van Rossum --- Lib/traceback.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/traceback.py b/Lib/traceback.py index 1a8fdd6e203bc0..c3214a502de647 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -682,7 +682,7 @@ def format(self, *, chain=True): If chain is not *True*, *__cause__* and *__context__* will not be formatted. - The return value is a generator of strings, each ending in a newline and + This is a generator of strings, each ending in a newline and some containing internal newlines. `print_exception_group` is a wrapper around this method which just prints the lines to a file. """ From 2e5cf257aad2ac69129fa032fd4a2a6db24ef07d Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Sun, 22 Nov 2020 21:55:15 +0000 Subject: [PATCH 58/73] Update Lib/traceback.py Co-authored-by: Guido van Rossum --- Lib/traceback.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/traceback.py b/Lib/traceback.py index c3214a502de647..d5ab4d0436d27b 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -680,7 +680,7 @@ def format(self, *, chain=True): followed by a printout of each exception in the group, which is expanded recursively. - If chain is not *True*, *__cause__* and *__context__* will not be formatted. + If chain is false(y), *__cause__* and *__context__* will not be formatted. This is a generator of strings, each ending in a newline and some containing internal newlines. `print_exception_group` is a wrapper From e14f9bbf8b9182648fc81c4ecc9eb2814a7be739 Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Sun, 22 Nov 2020 22:41:07 +0000 Subject: [PATCH 59/73] Guido's review comments (part1) --- Lib/exception_group.py | 21 ------------------ Lib/test/test_exception_group.py | 37 +++++++++++++------------------- Lib/test/test_traceback.py | 16 +++++++------- Lib/traceback.py | 23 +++++++++++--------- 4 files changed, 36 insertions(+), 61 deletions(-) diff --git a/Lib/exception_group.py b/Lib/exception_group.py index 60dcced05f0cb2..75ad52eb8da7d9 100644 --- a/Lib/exception_group.py +++ b/Lib/exception_group.py @@ -99,12 +99,6 @@ def subgroup(self, keep): match, _ = self.project(lambda e: e in keep) return match - @staticmethod - def render(exc, limit=None, file=None, chain=True): - traceback.print_exception( - type(exc), exc, exc.__traceback__, - limit=limit, file=file, chain=chain) - def __iter__(self): ''' iterate over the individual exceptions (flattens the tree) ''' for e in self.excs: @@ -125,21 +119,6 @@ def catch(types, handler): return ExceptionGroupCatcher(types, handler) -class StackGroupSummary(list): - @classmethod - def extract(klass, exc, *, indent=0, result=None, **kwargs): - - if result is None: - result = klass() - te = traceback.TracebackException.from_exception(exc, **kwargs) - result.append([indent, te]) - if isinstance(exc, ExceptionGroup): - for e in exc.excs: - StackGroupSummary.extract( - e, indent=indent+4, result=result, **kwargs) - return result - - class ExceptionGroupCatcher: """ Based on trio.MultiErrorCatcher """ diff --git a/Lib/test/test_exception_group.py b/Lib/test/test_exception_group.py index ccaf6c7185487c..8344d1d0e4adaf 100644 --- a/Lib/test/test_exception_group.py +++ b/Lib/test/test_exception_group.py @@ -194,9 +194,9 @@ def test_simple(self): expected = [ # (indent, exception) pairs (0, eg), - (4, eg.excs[0]), - (4, eg.excs[1]), - (4, eg.excs[2]), + (1, eg.excs[0]), + (1, eg.excs[1]), + (1, eg.excs[2]), ] self.check_summary_format_and_render(eg, expected) @@ -204,7 +204,6 @@ def test_simple(self): def check_summary_format_and_render(self, eg, expected): makeTE = traceback.TracebackException.from_exception - # StackGroupSummary.extract summary = traceback.TracebackExceptionGroup.from_exception(eg).summary self.assertEqual(len(expected), len(summary)) self.assertEqual([e[0] for e in summary], @@ -212,32 +211,26 @@ def check_summary_format_and_render(self, eg, expected): self.assertEqual([e[1] for e in summary], [makeTE(e) for e in [e[1] for e in expected]]) - # ExceptionGroup.format + # smoke test for traceback.TracebackExceptionGroup.format format_output = list(traceback.TracebackExceptionGroup.from_exception(eg).format()) - render_output = StringIO() - ExceptionGroup.render(eg, file=render_output) - self.assertIsInstance(format_output, list) - self.assertIsInstance(render_output.getvalue(), str) - self.assertEqual("".join(format_output).replace('\n', ''), - render_output.getvalue().replace('\n', '')) def test_stack_summary_nested(self): eg = self.newNestedEG(15) expected = [ # (indent, exception) pairs (0, eg), - (4, eg.excs[0]), - (8, eg.excs[0].excs[0]), - (12, eg.excs[0].excs[0].excs[0]), - (12, eg.excs[0].excs[0].excs[1]), - (12, eg.excs[0].excs[0].excs[2]), - (8, eg.excs[0].excs[1]), - (12, eg.excs[0].excs[1].excs[0]), - (12, eg.excs[0].excs[1].excs[1]), - (12, eg.excs[0].excs[1].excs[2]), - (8, eg.excs[0].excs[2]), - (4, eg.excs[1]), + (1, eg.excs[0]), + (2, eg.excs[0].excs[0]), + (3, eg.excs[0].excs[0].excs[0]), + (3, eg.excs[0].excs[0].excs[1]), + (3, eg.excs[0].excs[0].excs[2]), + (2, eg.excs[0].excs[1]), + (3, eg.excs[0].excs[1].excs[0]), + (3, eg.excs[0].excs[1].excs[1]), + (3, eg.excs[0].excs[1].excs[2]), + (2, eg.excs[0].excs[2]), + (1, eg.excs[1]), ] self.check_summary_format_and_render(eg, expected) diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index 6687f0e1219653..6a2e3fee4708cf 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -1278,7 +1278,7 @@ def foo(): exc_info = sys.exc_info() teg = traceback.TracebackExceptionGroup(*exc_info) exc = traceback.TracebackException(*exc_info) - self.assertEqual(teg.summary, ((0, exc),)) + self.assertEqual(teg.summary, [(0, exc)]) try: foo() @@ -1288,7 +1288,7 @@ def foo(): e, limit=1, lookup_lines=False, capture_locals=True), traceback.TracebackException.from_exception( e, limit=1, lookup_lines=False, capture_locals=True)) - self.assertEqual(teg.summary, ((0, exc),)) + self.assertEqual(teg.summary, [(0, exc)]) def test_exception_group(self): def f(): @@ -1322,13 +1322,13 @@ def g(v): exc = traceback.TracebackExceptionGroup(*exc_info) expected_stack = traceback.StackSummary.extract( traceback.walk_tb(exc_info[2])) - expected_summary = ( + expected_summary = [ (0, traceback.TracebackException(*exc_info)), - (4, traceback.TracebackException.from_exception(exc3)), - (8, traceback.TracebackException.from_exception(exc1)), - (8, traceback.TracebackException.from_exception(exc2)), - (4, traceback.TracebackException.from_exception(exc4)), - ) + (1, traceback.TracebackException.from_exception(exc3)), + (2, traceback.TracebackException.from_exception(exc1)), + (2, traceback.TracebackException.from_exception(exc2)), + (1, traceback.TracebackException.from_exception(exc4)), + ] self.assertEqual(exc.summary, expected_summary) formatted_exception_only = list(exc.format_exception_only()) diff --git a/Lib/traceback.py b/Lib/traceback.py index d5ab4d0436d27b..73cf490049ca0d 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -613,9 +613,10 @@ def format(self, *, chain=True): If chain is not *True*, *__cause__* and *__context__* will not be formatted. - The return value is a generator of strings, each ending in a newline and - some containing internal newlines. `print_exception` is a wrapper around - this method which just prints the lines to a file. + The return value is a generator of strings, each ending in a newline + and some containing internal newlines. `print_exception(e)`, when `e` + is a single exception (rather than an ExceptionGroup), is essentially + a wrapper around this method which just prints the lines to a file. The message indicating which exception occurred is always the last string in the output. @@ -649,7 +650,8 @@ class TracebackExceptionGroup: """ def __init__(self, exc_type, exc_value, exc_traceback, **kwargs): - self.summary = tuple( + self.indent_size = 4 + self.summary = list( self._gen_summary( exc_type, exc_value, exc_traceback, **kwargs)) self._str = _some_str(exc_value) @@ -666,7 +668,7 @@ def _gen_summary(self, exc_type, exc_value, exc_traceback, if isinstance(exc_value, exception_group.ExceptionGroup): for e in exc_value.excs: yield from self._gen_summary( - type(e), e, e.__traceback__, indent=indent+4, **kwargs) + type(e), e, e.__traceback__, indent=indent+1, **kwargs) @classmethod def from_exception(cls, exc, *args, **kwargs): @@ -682,8 +684,8 @@ def format(self, *, chain=True): If chain is false(y), *__cause__* and *__context__* will not be formatted. - This is a generator of strings, each ending in a newline and - some containing internal newlines. `print_exception_group` is a wrapper + This is a generator of strings, each ending in a newline + and some containing internal newlines. `print_exception` is a wrapper around this method which just prints the lines to a file. """ # TODO: should we add an arg to bound the number of exceptions printed? @@ -692,8 +694,8 @@ def format(self, *, chain=True): if indent == 0: yield from te.format(chain=chain) else: - indent_str = ' '*indent - yield indent_str + '-'*(60-indent) +'\n' + indent_str = ' '*self.indent_size*indent + yield indent_str + '-'*(60-len(indent_str)) +'\n' yield textwrap.indent( ''.join(list(te.format(chain=chain))), indent_str) @@ -702,8 +704,9 @@ def format_exception_only(self): if indent == 0: yield from te.format_exception_only() else: + indent_str = ' '*self.indent_size*indent yield textwrap.indent( - ''.join(list(te.format_exception_only())),' '*indent) + ''.join(list(te.format_exception_only())), indent_str) def __eq__(self, other): if isinstance(other, TracebackExceptionGroup): From bc38881b7194401b0d755c31b50426bb85510f46 Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Mon, 23 Nov 2020 00:16:03 +0000 Subject: [PATCH 60/73] move message to be first arg of ExceptionGroup() --- Lib/exception_group.py | 16 +++++------ Lib/test/test_exception_group.py | 47 +++++++++++++++++++------------- Lib/test/test_traceback.py | 4 +-- 3 files changed, 38 insertions(+), 29 deletions(-) diff --git a/Lib/exception_group.py b/Lib/exception_group.py index 75ad52eb8da7d9..2a9b5710810b45 100644 --- a/Lib/exception_group.py +++ b/Lib/exception_group.py @@ -21,15 +21,17 @@ def __init__(self, excs): class ExceptionGroup(BaseException): - def __init__(self, excs, message=None, *, tb=None): + def __init__(self, message, excs, *, tb=None): """ Construct a new ExceptionGroup excs: sequence of exceptions tb [optional]: the __traceback__ of this exception group. Typically set when this ExceptionGroup is derived from another. """ - self.excs = excs + if message is not None and not isinstance(message, (str, bytes)): + raise ValueError("message must be a string or None") self.message = message + self.excs = excs super().__init__(self.message) # self.__traceback__ is updated as usual, but self.__traceback_group__ # is set when the exception group is created. @@ -67,16 +69,15 @@ def project(self, condition, with_complement=False): elif with_complement: rest.append(e) - match_exc = ExceptionGroup(match, tb=self.__traceback__) + match_exc = ExceptionGroup(self.message, match, tb=self.__traceback__) def copy_metadata(src, target): - target.message = src.message target.__context__ = src.__context__ target.__cause__ = src.__cause__ copy_metadata(self, match_exc) if with_complement: - rest_exc = ExceptionGroup(rest, tb=self.__traceback__) + rest_exc = ExceptionGroup(self.message, rest, tb=self.__traceback__) copy_metadata(self, rest_exc) else: rest_exc = None @@ -112,7 +113,7 @@ def is_empty(self): return not any(self) def __repr__(self): - return f"ExceptionGroup({self.excs})" + return f"ExceptionGroup({self.message}, {self.excs})" @staticmethod def catch(types, handler): @@ -178,8 +179,7 @@ def __exit__(self, etype, exc, tb): to_add = handler_excs.subgroup( [e for e in handler_excs if e not in match]) if not to_add.is_empty(): - to_raise = ExceptionGroup([to_keep, to_add]) - to_raise.message = exc.message + to_raise = ExceptionGroup(exc.message, [to_keep, to_add]) else: to_raise = to_keep diff --git a/Lib/test/test_exception_group.py b/Lib/test/test_exception_group.py index 8344d1d0e4adaf..8978525825fad1 100644 --- a/Lib/test/test_exception_group.py +++ b/Lib/test/test_exception_group.py @@ -35,7 +35,7 @@ def tracebackGroupSanityCheck(self, exc): class ExceptionGroupTestUtils(ExceptionGroupTestBase): - def newEG(self, raisers, message=None): + def newEG(self, message, raisers): excs = [] for r in raisers: try: @@ -43,7 +43,7 @@ def newEG(self, raisers, message=None): except (Exception, ExceptionGroup) as e: excs.append(e) try: - raise ExceptionGroup(excs, message=message) + raise ExceptionGroup(message, excs) except ExceptionGroup as e: return e @@ -56,36 +56,39 @@ def newTE(self, t): def newSimpleEG(self, message=None): bind = functools.partial return self.newEG( + message, [bind(self.newVE, 1), bind(self.newTE, int), - bind(self.newVE, 2), ], - message=message) + bind(self.newVE, 2), ]) def newNestedEG(self, arg, message=None): bind = functools.partial def level1(i): - return self.newEG([ - bind(self.newVE, i), - bind(self.newTE, int), - bind(self.newVE, i+1), - ]) + return self.newEG( + 'msg', + [bind(self.newVE, i), + bind(self.newTE, int), + bind(self.newVE, i+1), + ]) def raiseExc(e): raise e def level2(i): - return self.newEG([ - bind(raiseExc, level1(i)), - bind(raiseExc, level1(i+1)), - bind(self.newVE, i+2), - ]) + return self.newEG( + 'msg', + [bind(raiseExc, level1(i)), + bind(raiseExc, level1(i+1)), + bind(self.newVE, i+2), + ]) def level3(i): - return self.newEG([ - bind(raiseExc, level2(i+1)), - bind(self.newVE, i+2), - ]) + return self.newEG( + 'msg', + [bind(raiseExc, level2(i+1)), + bind(self.newVE, i+2), + ]) return level3(arg) @@ -431,8 +434,10 @@ def test_catch_handler_adds_new_exceptions(self): class Handler(self.BaseHandler): def handle(self, eg): raise ExceptionGroup( + "msg1", [ValueError('foo'), ExceptionGroup( + "msg2", [SyntaxError('bar'), ValueError('baz')])]) newErrors_template = [ @@ -507,9 +512,11 @@ def test_catch_handler_reraise_new_and_all_old(self): class Handler(self.BaseHandler): def handle(self, eg): raise ExceptionGroup( + "msg1", [eg, ValueError('foo'), ExceptionGroup( + "msg2", [SyntaxError('bar'), ValueError('baz')])]) newErrors_template = [ @@ -534,10 +541,12 @@ def test_catch_handler_reraise_new_and_some_old(self): class Handler(self.BaseHandler): def handle(self, eg): raise ExceptionGroup( + "msg1", [eg.excs[0], ValueError('foo'), ExceptionGroup( - [SyntaxError('bar'), ValueError('baz')])]) + "msg2", + [SyntaxError('bar'), ValueError('baz')])]) newErrors_template = [ ValueError('foo'), [SyntaxError('bar'), ValueError('baz')]] diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index 6a2e3fee4708cf..260e407085674e 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -1308,7 +1308,7 @@ def g(v): except Exception as e: exc2 = e raise exception_group.ExceptionGroup( - [exc1, exc2], message="eg1") + "eg1", [exc1, exc2]) except exception_group.ExceptionGroup as e: exc3 = e try: @@ -1316,7 +1316,7 @@ def g(v): except Exception as e: exc4 = e raise exception_group.ExceptionGroup( - [exc3, exc4], message="eg2") + "eg2", [exc3, exc4]) except exception_group.ExceptionGroup as eg: exc_info = sys.exc_info() exc = traceback.TracebackExceptionGroup(*exc_info) From 5218614feb9955c6e9184545cb6faa3927bcfdc8 Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Mon, 23 Nov 2020 14:38:37 +0000 Subject: [PATCH 61/73] more of Guido's review comments --- Lib/exception_group.py | 3 +-- Lib/test/test_traceback.py | 8 ++++---- Lib/traceback.py | 17 ++++++++++++----- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/Lib/exception_group.py b/Lib/exception_group.py index 2a9b5710810b45..684068386fff71 100644 --- a/Lib/exception_group.py +++ b/Lib/exception_group.py @@ -28,8 +28,7 @@ def __init__(self, message, excs, *, tb=None): tb [optional]: the __traceback__ of this exception group. Typically set when this ExceptionGroup is derived from another. """ - if message is not None and not isinstance(message, (str, bytes)): - raise ValueError("message must be a string or None") + assert message is None or isinstance(message, str) self.message = message self.excs = excs super().__init__(self.message) diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index 260e407085674e..5d10fc17c38cd7 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -1352,13 +1352,13 @@ def g(v): f'line {lineno_g+21}, in test_exception_group', ' raise exception_group.ExceptionGroup(', 'exception_group.ExceptionGroup: eg2', - ' --------------------------------------------------------', + ' ------------------------------------------------------------', ' Traceback (most recent call last):', f' File "{__file__}", ' f'line {lineno_g+13}, in test_exception_group', ' raise exception_group.ExceptionGroup(', ' exception_group.ExceptionGroup: eg1', - ' ----------------------------------------------------', + ' ------------------------------------------------------------', ' Traceback (most recent call last):', ' File ' f'"{__file__}", line {lineno_g+6}, in ' @@ -1369,7 +1369,7 @@ def g(v): 'f', ' 1/0', ' ZeroDivisionError: division by zero', - ' ----------------------------------------------------', + ' ------------------------------------------------------------', ' Traceback (most recent call last):', ' File ' f'"{__file__}", line {lineno_g+10}, in ' @@ -1380,7 +1380,7 @@ def g(v): 'g', ' raise ValueError(v)', ' ValueError: 42', - ' --------------------------------------------------------', + ' ------------------------------------------------------------', ' Traceback (most recent call last):', f' File "{__file__}", ' f'line {lineno_g+18}, in test_exception_group', diff --git a/Lib/traceback.py b/Lib/traceback.py index 73cf490049ca0d..78a9631f9136dd 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -648,6 +648,7 @@ class TracebackExceptionGroup: - :attr:`summary` A tuple of (indentation level, TracebackException) pairs """ + SEPARATOR_LINE = '-'*60+'\n' def __init__(self, exc_type, exc_value, exc_traceback, **kwargs): self.indent_size = 4 @@ -688,15 +689,15 @@ def format(self, *, chain=True): and some containing internal newlines. `print_exception` is a wrapper around this method which just prints the lines to a file. """ - # TODO: should we add an arg to bound the number of exceptions printed? - # or should we use limit somehow for that? + # TODO: Add two args to bound (1) the depth of exceptions reported, and + # (2) the number of exceptions reported per level for indent, te in self.summary: if indent == 0: yield from te.format(chain=chain) else: indent_str = ' '*self.indent_size*indent - yield indent_str + '-'*(60-len(indent_str)) +'\n' - yield textwrap.indent( + yield indent_str + self.SEPARATOR_LINE + yield self._indent( ''.join(list(te.format(chain=chain))), indent_str) def format_exception_only(self): @@ -705,9 +706,15 @@ def format_exception_only(self): yield from te.format_exception_only() else: indent_str = ' '*self.indent_size*indent - yield textwrap.indent( + yield self._indent( ''.join(list(te.format_exception_only())), indent_str) + def _indent(self, line, indent_str): + if '\n' not in line: + return indent_str + line + else: + return textwrap.indent(line, indent_str) + def __eq__(self, other): if isinstance(other, TracebackExceptionGroup): return self.__dict__ == other.__dict__ From 942ac86da6653fa0519bfcbb0559e758ec35ca4a Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Mon, 23 Nov 2020 16:00:56 +0000 Subject: [PATCH 62/73] add indent_level field to TracebackException --- Lib/test/test_exception_group.py | 7 ++-- Lib/test/test_traceback.py | 22 +++++------ Lib/traceback.py | 64 ++++++++++++++++++-------------- 3 files changed, 51 insertions(+), 42 deletions(-) diff --git a/Lib/test/test_exception_group.py b/Lib/test/test_exception_group.py index 8978525825fad1..4969f225fab794 100644 --- a/Lib/test/test_exception_group.py +++ b/Lib/test/test_exception_group.py @@ -209,10 +209,9 @@ def check_summary_format_and_render(self, eg, expected): summary = traceback.TracebackExceptionGroup.from_exception(eg).summary self.assertEqual(len(expected), len(summary)) - self.assertEqual([e[0] for e in summary], - [e[0] for e in expected]) - self.assertEqual([e[1] for e in summary], - [makeTE(e) for e in [e[1] for e in expected]]) + self.assertEqual([e.indent_level for e in summary], [e[0] for e in expected]) + self.assertEqual([e for e in summary], + [makeTE(e[1], indent_level=e[0]) for e in expected]) # smoke test for traceback.TracebackExceptionGroup.format format_output = list(traceback.TracebackExceptionGroup.from_exception(eg).format()) diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index 5d10fc17c38cd7..ab0148ffb68b67 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -1278,7 +1278,7 @@ def foo(): exc_info = sys.exc_info() teg = traceback.TracebackExceptionGroup(*exc_info) exc = traceback.TracebackException(*exc_info) - self.assertEqual(teg.summary, [(0, exc)]) + self.assertEqual(teg.summary, [exc]) try: foo() @@ -1288,7 +1288,7 @@ def foo(): e, limit=1, lookup_lines=False, capture_locals=True), traceback.TracebackException.from_exception( e, limit=1, lookup_lines=False, capture_locals=True)) - self.assertEqual(teg.summary, [(0, exc)]) + self.assertEqual(teg.summary, [exc]) def test_exception_group(self): def f(): @@ -1323,11 +1323,11 @@ def g(v): expected_stack = traceback.StackSummary.extract( traceback.walk_tb(exc_info[2])) expected_summary = [ - (0, traceback.TracebackException(*exc_info)), - (1, traceback.TracebackException.from_exception(exc3)), - (2, traceback.TracebackException.from_exception(exc1)), - (2, traceback.TracebackException.from_exception(exc2)), - (1, traceback.TracebackException.from_exception(exc4)), + traceback.TracebackException(*exc_info, indent_level=0), + traceback.TracebackException.from_exception(exc3, indent_level=1), + traceback.TracebackException.from_exception(exc1, indent_level=2), + traceback.TracebackException.from_exception(exc2, indent_level=2), + traceback.TracebackException.from_exception(exc4, indent_level=1), ] self.assertEqual(exc.summary, expected_summary) formatted_exception_only = list(exc.format_exception_only()) @@ -1437,7 +1437,7 @@ def recurse(n): exc = traceback.TracebackExceptionGroup(*exc_info, limit=5) expected_stack = traceback.StackSummary.extract( traceback.walk_tb(exc_info[2]), limit=5) - self.assertEqual(expected_stack, exc.summary[0][1].stack) + self.assertEqual(expected_stack, exc.summary[0].stack) def test_lookup_lines(self): linecache.clearcache() @@ -1448,7 +1448,7 @@ def test_lookup_lines(self): exc = traceback.TracebackExceptionGroup(Exception, e, tb, lookup_lines=False) self.assertEqual({}, linecache.cache) linecache.updatecache('/foo.py', globals()) - self.assertEqual(exc.summary[0][1].stack[0].line, "import sys") + self.assertEqual(exc.summary[0].stack[0].line, "import sys") def test_locals(self): linecache.updatecache('/foo.py', globals()) @@ -1459,7 +1459,7 @@ def test_locals(self): exc = traceback.TracebackExceptionGroup( Exception, e, tb, capture_locals=True) self.assertEqual( - exc.summary[0][1].stack[0].locals, {'something': '1', 'other': "'string'"}) + exc.summary[0].stack[0].locals, {'something': '1', 'other': "'string'"}) def test_no_locals(self): linecache.updatecache('/foo.py', globals()) @@ -1468,7 +1468,7 @@ def test_no_locals(self): f = test_frame(c, globals(), {'something': 1}) tb = test_tb(f, 6, None) exc = traceback.TracebackExceptionGroup(Exception, e, tb) - self.assertEqual(exc.summary[0][1].stack[0].locals, None) + self.assertEqual(exc.summary[0].stack[0].locals, None) def test_traceback_header(self): # do not print a traceback header if exc_traceback is None diff --git a/Lib/traceback.py b/Lib/traceback.py index 78a9631f9136dd..2e7d266adeefbc 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -478,7 +478,7 @@ class TracebackException: """ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None, - lookup_lines=True, capture_locals=False, _seen=None): + lookup_lines=True, capture_locals=False, _seen=None, indent_level=0): # NB: we need to accept exc_traceback, exc_value, exc_traceback to # permit backwards compat with the existing API, otherwise we # need stub thunk objects just to glue it together. @@ -533,6 +533,7 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None, self.msg = exc_value.msg if lookup_lines: self._load_lines() + self.indent_level = indent_level @classmethod def from_exception(cls, exc, *args, **kwargs): @@ -638,38 +639,45 @@ def format(self, *, chain=True): class TracebackExceptionGroup: """An exception or exception group ready for rendering. - We capture enough attributes from the original exception - group to this intermediary form to ensure that no references are held, while - still being able to fully print or format it. + We capture enough attributes from the original exception group to this + intermediary form to ensure that no references are held, while still being + able to fully print or format it. Use `from_exception()` to create TracebackExceptionGroup instances from exception objects, or the constructor to create TracebackExceptionGroup instances from individual components. - - :attr:`summary` A tuple of (indentation level, TracebackException) pairs + - :attr:`summary` A list of TracebackException objects. For a single exception + the list has length 1, and for an ExceptionGroup it has an item for each exception + in the group (recursively), in DFS pre-order sequence: The first item represents + the given EG, the second represents the first exception in this EG's list, and + so on. """ + SEPARATOR_LINE = '-'*60+'\n' - def __init__(self, exc_type, exc_value, exc_traceback, **kwargs): + def __init__(self, exc_type, exc_value, exc_traceback, + indent_level=0, **kwargs): self.indent_size = 4 self.summary = list( self._gen_summary( - exc_type, exc_value, exc_traceback, **kwargs)) + exc_type, exc_value, exc_traceback, + indent_level = indent_level, **kwargs)) self._str = _some_str(exc_value) def _gen_summary(self, exc_type, exc_value, exc_traceback, - indent=0, **kwargs): + indent_level, **kwargs): # recursively extract the sequence of (indent, TracebackException) # pairs. If exc_value is a single exception, there is only one pair # (with indent 0). If it is an exception group, we get one pair # for each exception in it, showing the whole tree - te = TracebackException( - exc_type, exc_value, exc_traceback, **kwargs) - yield tuple((indent, te)) + yield TracebackException( + exc_type, exc_value, exc_traceback, + indent_level=indent_level, **kwargs) if isinstance(exc_value, exception_group.ExceptionGroup): for e in exc_value.excs: yield from self._gen_summary( - type(e), e, e.__traceback__, indent=indent+1, **kwargs) + type(e), e, e.__traceback__, indent_level+1, **kwargs) @classmethod def from_exception(cls, exc, *args, **kwargs): @@ -691,29 +699,31 @@ def format(self, *, chain=True): """ # TODO: Add two args to bound (1) the depth of exceptions reported, and # (2) the number of exceptions reported per level - for indent, te in self.summary: - if indent == 0: - yield from te.format(chain=chain) + for te in self.summary: + line_gen = te.format(chain=chain) + if te.indent_level == 0: + yield from line_gen else: - indent_str = ' '*self.indent_size*indent - yield indent_str + self.SEPARATOR_LINE - yield self._indent( - ''.join(list(te.format(chain=chain))), indent_str) + yield from self._format( + line_gen, te.indent_level, sep = self.SEPARATOR_LINE) def format_exception_only(self): - for indent, te in self.summary: - if indent == 0: - yield from te.format_exception_only() + for te in self.summary: + line_gen = te.format_exception_only() + if te.indent_level == 0: + yield from line_gen else: - indent_str = ' '*self.indent_size*indent - yield self._indent( - ''.join(list(te.format_exception_only())), indent_str) + yield from self._format(line_gen, te.indent_level) - def _indent(self, line, indent_str): + def _format(self, line_gen, indent_level, sep=None): + line = ''.join(list(line_gen)) + indent_str = ' ' * self.indent_size * indent_level if '\n' not in line: return indent_str + line else: - return textwrap.indent(line, indent_str) + if sep is not None: + yield indent_str + sep + yield textwrap.indent(line, indent_str) def __eq__(self, other): if isinstance(other, TracebackExceptionGroup): From 51405d2a7ca7ec5a1b5781af925aeade8a0edeba Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Mon, 23 Nov 2020 17:35:41 +0000 Subject: [PATCH 63/73] ExceptionGroup takes exceptions as *args instead of list --- Lib/exception_group.py | 10 ++++++---- Lib/test/test_exception_group.py | 27 ++++++++++++--------------- Lib/test/test_traceback.py | 4 ++-- 3 files changed, 20 insertions(+), 21 deletions(-) diff --git a/Lib/exception_group.py b/Lib/exception_group.py index 684068386fff71..d26f3f7484402f 100644 --- a/Lib/exception_group.py +++ b/Lib/exception_group.py @@ -21,7 +21,7 @@ def __init__(self, excs): class ExceptionGroup(BaseException): - def __init__(self, message, excs, *, tb=None): + def __init__(self, message, *excs, tb=None): """ Construct a new ExceptionGroup excs: sequence of exceptions @@ -29,6 +29,8 @@ def __init__(self, message, excs, *, tb=None): Typically set when this ExceptionGroup is derived from another. """ assert message is None or isinstance(message, str) + for e in excs: + assert isinstance(e, BaseException) self.message = message self.excs = excs super().__init__(self.message) @@ -68,7 +70,7 @@ def project(self, condition, with_complement=False): elif with_complement: rest.append(e) - match_exc = ExceptionGroup(self.message, match, tb=self.__traceback__) + match_exc = ExceptionGroup(self.message, *match, tb=self.__traceback__) def copy_metadata(src, target): target.__context__ = src.__context__ @@ -76,7 +78,7 @@ def copy_metadata(src, target): copy_metadata(self, match_exc) if with_complement: - rest_exc = ExceptionGroup(self.message, rest, tb=self.__traceback__) + rest_exc = ExceptionGroup(self.message, *rest, tb=self.__traceback__) copy_metadata(self, rest_exc) else: rest_exc = None @@ -178,7 +180,7 @@ def __exit__(self, etype, exc, tb): to_add = handler_excs.subgroup( [e for e in handler_excs if e not in match]) if not to_add.is_empty(): - to_raise = ExceptionGroup(exc.message, [to_keep, to_add]) + to_raise = ExceptionGroup(exc.message, to_keep, to_add) else: to_raise = to_keep diff --git a/Lib/test/test_exception_group.py b/Lib/test/test_exception_group.py index 4969f225fab794..06772221c0ae3d 100644 --- a/Lib/test/test_exception_group.py +++ b/Lib/test/test_exception_group.py @@ -43,7 +43,7 @@ def newEG(self, message, raisers): except (Exception, ExceptionGroup) as e: excs.append(e) try: - raise ExceptionGroup(message, excs) + raise ExceptionGroup(message, *excs) except ExceptionGroup as e: return e @@ -434,10 +434,9 @@ class Handler(self.BaseHandler): def handle(self, eg): raise ExceptionGroup( "msg1", - [ValueError('foo'), - ExceptionGroup( - "msg2", - [SyntaxError('bar'), ValueError('baz')])]) + ValueError('foo'), + ExceptionGroup( + "msg2",SyntaxError('bar'), ValueError('baz'))) newErrors_template = [ ValueError('foo'), [SyntaxError('bar'), ValueError('baz')]] @@ -512,11 +511,10 @@ class Handler(self.BaseHandler): def handle(self, eg): raise ExceptionGroup( "msg1", - [eg, - ValueError('foo'), - ExceptionGroup( - "msg2", - [SyntaxError('bar'), ValueError('baz')])]) + eg, + ValueError('foo'), + ExceptionGroup( + "msg2", SyntaxError('bar'), ValueError('baz'))) newErrors_template = [ ValueError('foo'), [SyntaxError('bar'), ValueError('baz')]] @@ -541,11 +539,10 @@ class Handler(self.BaseHandler): def handle(self, eg): raise ExceptionGroup( "msg1", - [eg.excs[0], - ValueError('foo'), - ExceptionGroup( - "msg2", - [SyntaxError('bar'), ValueError('baz')])]) + eg.excs[0], + ValueError('foo'), + ExceptionGroup( + "msg2", SyntaxError('bar'), ValueError('baz'))) newErrors_template = [ ValueError('foo'), [SyntaxError('bar'), ValueError('baz')]] diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index ab0148ffb68b67..4596a6d2d9e3b6 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -1308,7 +1308,7 @@ def g(v): except Exception as e: exc2 = e raise exception_group.ExceptionGroup( - "eg1", [exc1, exc2]) + "eg1", exc1, exc2) except exception_group.ExceptionGroup as e: exc3 = e try: @@ -1316,7 +1316,7 @@ def g(v): except Exception as e: exc4 = e raise exception_group.ExceptionGroup( - "eg2", [exc3, exc4]) + "eg2", exc3, exc4) except exception_group.ExceptionGroup as eg: exc_info = sys.exc_info() exc = traceback.TracebackExceptionGroup(*exc_info) From 46117d3c9c6c53ca958881a9440c059c20382998 Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Mon, 23 Nov 2020 19:05:03 +0000 Subject: [PATCH 64/73] make indent 3 instead of 4 --- Lib/test/test_traceback.py | 132 +++++++++++++++++-------------------- Lib/traceback.py | 46 ++++++------- 2 files changed, 83 insertions(+), 95 deletions(-) diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index 4596a6d2d9e3b6..abeb1f6776222b 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -338,16 +338,16 @@ def f(): else: self.fail("no recursion occurred") - lineno_f = f.__code__.co_firstlineno + lno_f = f.__code__.co_firstlineno result_f = ( 'Traceback (most recent call last):\n' - f' File "{__file__}", line {lineno_f+5}, in _check_recursive_traceback_display\n' + f' File "{__file__}", line {lno_f+5}, in _check_recursive_traceback_display\n' ' f()\n' - f' File "{__file__}", line {lineno_f+1}, in f\n' + f' File "{__file__}", line {lno_f+1}, in f\n' ' f()\n' - f' File "{__file__}", line {lineno_f+1}, in f\n' + f' File "{__file__}", line {lno_f+1}, in f\n' ' f()\n' - f' File "{__file__}", line {lineno_f+1}, in f\n' + f' File "{__file__}", line {lno_f+1}, in f\n' ' f()\n' # XXX: The following line changes depending on whether the tests # are run through the interactive interpreter or with -m @@ -385,22 +385,22 @@ def g(count=10): else: self.fail("no value error was raised") - lineno_g = g.__code__.co_firstlineno + lno_g = g.__code__.co_firstlineno result_g = ( - f' File "{__file__}", line {lineno_g+2}, in g\n' + f' File "{__file__}", line {lno_g+2}, in g\n' ' return g(count-1)\n' - f' File "{__file__}", line {lineno_g+2}, in g\n' + f' File "{__file__}", line {lno_g+2}, in g\n' ' return g(count-1)\n' - f' File "{__file__}", line {lineno_g+2}, in g\n' + f' File "{__file__}", line {lno_g+2}, in g\n' ' return g(count-1)\n' ' [Previous line repeated 7 more times]\n' - f' File "{__file__}", line {lineno_g+3}, in g\n' + f' File "{__file__}", line {lno_g+3}, in g\n' ' raise ValueError\n' 'ValueError\n' ) tb_line = ( 'Traceback (most recent call last):\n' - f' File "{__file__}", line {lineno_g+7}, in _check_recursive_traceback_display\n' + f' File "{__file__}", line {lno_g+7}, in _check_recursive_traceback_display\n' ' g()\n' ) expected = (tb_line + result_g).splitlines() @@ -449,19 +449,19 @@ def h(count=10): else: self.fail("no error raised") result_g = ( - f' File "{__file__}", line {lineno_g+2}, in g\n' + f' File "{__file__}", line {lno_g+2}, in g\n' ' return g(count-1)\n' - f' File "{__file__}", line {lineno_g+2}, in g\n' + f' File "{__file__}", line {lno_g+2}, in g\n' ' return g(count-1)\n' - f' File "{__file__}", line {lineno_g+2}, in g\n' + f' File "{__file__}", line {lno_g+2}, in g\n' ' return g(count-1)\n' - f' File "{__file__}", line {lineno_g+3}, in g\n' + f' File "{__file__}", line {lno_g+3}, in g\n' ' raise ValueError\n' 'ValueError\n' ) tb_line = ( 'Traceback (most recent call last):\n' - f' File "{__file__}", line {lineno_g+71}, in _check_recursive_traceback_display\n' + f' File "{__file__}", line {lno_g+71}, in _check_recursive_traceback_display\n' ' g(traceback._RECURSIVE_CUTOFF)\n' ) expected = (tb_line + result_g).splitlines() @@ -477,20 +477,20 @@ def h(count=10): else: self.fail("no error raised") result_g = ( - f' File "{__file__}", line {lineno_g+2}, in g\n' + f' File "{__file__}", line {lno_g+2}, in g\n' ' return g(count-1)\n' - f' File "{__file__}", line {lineno_g+2}, in g\n' + f' File "{__file__}", line {lno_g+2}, in g\n' ' return g(count-1)\n' - f' File "{__file__}", line {lineno_g+2}, in g\n' + f' File "{__file__}", line {lno_g+2}, in g\n' ' return g(count-1)\n' ' [Previous line repeated 1 more time]\n' - f' File "{__file__}", line {lineno_g+3}, in g\n' + f' File "{__file__}", line {lno_g+3}, in g\n' ' raise ValueError\n' 'ValueError\n' ) tb_line = ( 'Traceback (most recent call last):\n' - f' File "{__file__}", line {lineno_g+99}, in _check_recursive_traceback_display\n' + f' File "{__file__}", line {lno_g+99}, in _check_recursive_traceback_display\n' ' g(traceback._RECURSIVE_CUTOFF + 1)\n' ) expected = (tb_line + result_g).splitlines() @@ -1334,62 +1334,50 @@ def g(v): expected_exception_only = [ 'exception_group.ExceptionGroup: eg2\n', - ' exception_group.ExceptionGroup: eg1\n', - ' ZeroDivisionError: division by zero\n', - ' ValueError: 42\n', - ' ValueError: 24\n' + ' exception_group.ExceptionGroup: eg1\n', + ' ZeroDivisionError: division by zero\n', + ' ValueError: 42\n', + ' ValueError: 24\n' ] self.assertEqual(formatted_exception_only, expected_exception_only) formatted = ''.join(exc.format()).split('\n') - lineno_f = f.__code__.co_firstlineno - lineno_g = g.__code__.co_firstlineno - - expected = [ - 'Traceback (most recent call last):', - f' File "{__file__}", ' - f'line {lineno_g+21}, in test_exception_group', - ' raise exception_group.ExceptionGroup(', - 'exception_group.ExceptionGroup: eg2', - ' ------------------------------------------------------------', - ' Traceback (most recent call last):', - f' File "{__file__}", ' - f'line {lineno_g+13}, in test_exception_group', - ' raise exception_group.ExceptionGroup(', - ' exception_group.ExceptionGroup: eg1', - ' ------------------------------------------------------------', - ' Traceback (most recent call last):', - ' File ' - f'"{__file__}", line {lineno_g+6}, in ' - 'test_exception_group', - ' f()', - ' File ' - f'"{__file__}", line {lineno_f+1}, in ' - 'f', - ' 1/0', - ' ZeroDivisionError: division by zero', - ' ------------------------------------------------------------', - ' Traceback (most recent call last):', - ' File ' - f'"{__file__}", line {lineno_g+10}, in ' - 'test_exception_group', - ' g(42)', - ' File ' - f'"{__file__}", line {lineno_g+1}, in ' - 'g', - ' raise ValueError(v)', - ' ValueError: 42', - ' ------------------------------------------------------------', - ' Traceback (most recent call last):', - f' File "{__file__}", ' - f'line {lineno_g+18}, in test_exception_group', - ' g(24)', - f' File "{__file__}", ' - f'line {lineno_g+1}, in g', - ' raise ValueError(v)', - ' ValueError: 24', - ''] + lno_f = f.__code__.co_firstlineno + lno_g = g.__code__.co_firstlineno + + expected = textwrap.dedent(f"""\ + Traceback (most recent call last): + File "{__file__}", line {lno_g+21}, in test_exception_group + raise exception_group.ExceptionGroup( + exception_group.ExceptionGroup: eg2 + ------------------------------------------------------------ + Traceback (most recent call last): + File "{__file__}", line {lno_g+13}, in test_exception_group + raise exception_group.ExceptionGroup( + exception_group.ExceptionGroup: eg1 + ------------------------------------------------------------ + Traceback (most recent call last): + File "{__file__}", line {lno_g+6}, in test_exception_group + f() + File "{__file__}", line {lno_f+1}, in f + 1/0 + ZeroDivisionError: division by zero + ------------------------------------------------------------ + Traceback (most recent call last): + File "{__file__}", line {lno_g+10}, in test_exception_group + g(42) + File "{__file__}", line {lno_g+1}, in g + raise ValueError(v) + ValueError: 42 + ------------------------------------------------------------ + Traceback (most recent call last): + File "{__file__}", line {lno_g+18}, in test_exception_group + g(24) + File "{__file__}", line {lno_g+1}, in g + raise ValueError(v) + ValueError: 24 + """).split('\n') self.assertEqual(formatted, expected) diff --git a/Lib/traceback.py b/Lib/traceback.py index 2e7d266adeefbc..1aeecf06fb1a4d 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -654,11 +654,11 @@ class TracebackExceptionGroup: so on. """ - SEPARATOR_LINE = '-'*60+'\n' + SEPARATOR_LINE = '-' * 60 + '\n' + INDENT_SIZE = 3 def __init__(self, exc_type, exc_value, exc_traceback, indent_level=0, **kwargs): - self.indent_size = 4 self.summary = list( self._gen_summary( exc_type, exc_value, exc_traceback, @@ -677,7 +677,7 @@ def _gen_summary(self, exc_type, exc_value, exc_traceback, if isinstance(exc_value, exception_group.ExceptionGroup): for e in exc_value.excs: yield from self._gen_summary( - type(e), e, e.__traceback__, indent_level+1, **kwargs) + type(e), e, e.__traceback__, indent_level + 1, **kwargs) @classmethod def from_exception(cls, exc, *args, **kwargs): @@ -697,33 +697,33 @@ def format(self, *, chain=True): and some containing internal newlines. `print_exception` is a wrapper around this method which just prints the lines to a file. """ - # TODO: Add two args to bound (1) the depth of exceptions reported, and + # TODO: Add two args to bound - + # (1) the depth of exceptions reported, and # (2) the number of exceptions reported per level for te in self.summary: - line_gen = te.format(chain=chain) - if te.indent_level == 0: - yield from line_gen - else: - yield from self._format( - line_gen, te.indent_level, sep = self.SEPARATOR_LINE) + yield from self._format( + te.format(chain=chain), + te.indent_level, + sep=self.SEPARATOR_LINE) def format_exception_only(self): for te in self.summary: - line_gen = te.format_exception_only() - if te.indent_level == 0: - yield from line_gen - else: - yield from self._format(line_gen, te.indent_level) + yield from self._format( + te.format_exception_only(), + te.indent_level) - def _format(self, line_gen, indent_level, sep=None): - line = ''.join(list(line_gen)) - indent_str = ' ' * self.indent_size * indent_level - if '\n' not in line: - return indent_str + line + def _format(self, text_gen, indent_level, sep=None): + if indent_level == 0: + yield from text_gen else: - if sep is not None: - yield indent_str + sep - yield textwrap.indent(line, indent_str) + text = ''.join(list(text_gen)) + indent_str = ' ' * self.INDENT_SIZE * indent_level + if '\n' not in text: + yield indent_str + text + else: + if sep is not None: + yield indent_str + sep + yield textwrap.indent(text, indent_str) def __eq__(self, other): if isinstance(other, TracebackExceptionGroup): From 7a3df2ad265bf8cffc9a80a55886ca4e0e0f7848 Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Mon, 23 Nov 2020 22:04:26 +0000 Subject: [PATCH 65/73] remove tb arg from ExceptionGroup --- Lib/exception_group.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Lib/exception_group.py b/Lib/exception_group.py index d26f3f7484402f..8731f975d7f13a 100644 --- a/Lib/exception_group.py +++ b/Lib/exception_group.py @@ -21,7 +21,7 @@ def __init__(self, excs): class ExceptionGroup(BaseException): - def __init__(self, message, *excs, tb=None): + def __init__(self, message, *excs): """ Construct a new ExceptionGroup excs: sequence of exceptions @@ -29,15 +29,14 @@ def __init__(self, message, *excs, tb=None): Typically set when this ExceptionGroup is derived from another. """ assert message is None or isinstance(message, str) - for e in excs: - assert isinstance(e, BaseException) + assert all(isinstance(e, BaseException) for e in excs) + self.message = message self.excs = excs super().__init__(self.message) # self.__traceback__ is updated as usual, but self.__traceback_group__ # is set when the exception group is created. # __traceback_group__ and __traceback__ combine to give the full path. - self.__traceback__ = tb self.__traceback_group__ = TracebackGroup(self.excs) def project(self, condition, with_complement=False): @@ -70,7 +69,8 @@ def project(self, condition, with_complement=False): elif with_complement: rest.append(e) - match_exc = ExceptionGroup(self.message, *match, tb=self.__traceback__) + match_exc = ExceptionGroup( + self.message, *match).with_traceback(self.__traceback__) def copy_metadata(src, target): target.__context__ = src.__context__ @@ -78,7 +78,7 @@ def copy_metadata(src, target): copy_metadata(self, match_exc) if with_complement: - rest_exc = ExceptionGroup(self.message, *rest, tb=self.__traceback__) + rest_exc = ExceptionGroup(self.message, *rest).with_traceback(self.__traceback__) copy_metadata(self, rest_exc) else: rest_exc = None From 3d25ae70209214b3c96301f3248bab9ab92002eb Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Tue, 24 Nov 2020 01:10:35 +0000 Subject: [PATCH 66/73] break up large test function, remove some of the tests that should not have been copied over from TracebackException. --- Lib/test/test_traceback.py | 235 ++++++++++++++++++------------------- 1 file changed, 112 insertions(+), 123 deletions(-) diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index abeb1f6776222b..0d6cf506cb39a5 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -1268,35 +1268,30 @@ def test_traceback_header(self): class TestTracebackGroupException(unittest.TestCase): + def setUp(self): + super().setUp() + self.exc_info = self._get_exception() + self.eg_info = self._get_exception_group() - def test_simple_exception(self): + def _get_exception(self): def foo(): 1/0 - try: - foo() - except Exception: - exc_info = sys.exc_info() - teg = traceback.TracebackExceptionGroup(*exc_info) - exc = traceback.TracebackException(*exc_info) - self.assertEqual(teg.summary, [exc]) - try: foo() except Exception as e: - teg, exc = ( - traceback.TracebackExceptionGroup.from_exception( - e, limit=1, lookup_lines=False, capture_locals=True), - traceback.TracebackException.from_exception( - e, limit=1, lookup_lines=False, capture_locals=True)) - self.assertEqual(teg.summary, [exc]) - - def test_exception_group(self): + return sys.exc_info() + self.assertFalse('Exception Not Raised') + + def _get_exception_group(self): def f(): 1/0 def g(v): raise ValueError(v) + self.lno_f = f.__code__.co_firstlineno + self.lno_g = g.__code__.co_firstlineno + try: try: try: @@ -1307,72 +1302,125 @@ def g(v): g(42) except Exception as e: exc2 = e - raise exception_group.ExceptionGroup( - "eg1", exc1, exc2) + raise exception_group.ExceptionGroup("eg1", exc1, exc2) except exception_group.ExceptionGroup as e: exc3 = e try: g(24) except Exception as e: exc4 = e - raise exception_group.ExceptionGroup( - "eg2", exc3, exc4) + raise exception_group.ExceptionGroup("eg2", exc3, exc4) except exception_group.ExceptionGroup as eg: - exc_info = sys.exc_info() - exc = traceback.TracebackExceptionGroup(*exc_info) - expected_stack = traceback.StackSummary.extract( - traceback.walk_tb(exc_info[2])) - expected_summary = [ - traceback.TracebackException(*exc_info, indent_level=0), - traceback.TracebackException.from_exception(exc3, indent_level=1), - traceback.TracebackException.from_exception(exc1, indent_level=2), - traceback.TracebackException.from_exception(exc2, indent_level=2), - traceback.TracebackException.from_exception(exc4, indent_level=1), + return sys.exc_info() + self.assertFalse('Exception Not Raised') + + def test_single_exception_constructor(self): + exc_info = self.exc_info + teg1 = traceback.TracebackExceptionGroup(*exc_info) + exc1 = traceback.TracebackException(*exc_info) + self.assertEqual(teg1.summary, [exc1]) + + teg2 = traceback.TracebackExceptionGroup(*exc_info, indent_level=5) + exc2 = traceback.TracebackException(*exc_info, indent_level=5) + not_exc = traceback.TracebackException(*exc_info, indent_level=6) + self.assertEqual(teg2.summary, [exc2]) + self.assertNotEqual(teg2.summary, [not_exc]) + + def test_single_exception_from_exception(self): + exc_info = self.exc_info + teg1 = traceback.TracebackExceptionGroup.from_exception(exc_info[1]) + exc1 = traceback.TracebackException(*exc_info) + self.assertEqual(teg1.summary, [exc1]) + + teg2 = traceback.TracebackExceptionGroup.from_exception( + exc_info[1], indent_level=5) + exc2 = traceback.TracebackException(*exc_info, indent_level=5) + not_exc = traceback.TracebackException(*exc_info, indent_level=6) + self.assertEqual(teg2.summary, [exc2]) + self.assertNotEqual(teg2.summary, [not_exc]) + + def test_exception_group_construction(self): + eg_info = self.eg_info + teg1 = traceback.TracebackExceptionGroup(*eg_info) + teg2 = traceback.TracebackExceptionGroup.from_exception(eg_info[1]) + self.assertIsNot(teg1, teg2) + self.assertEqual(teg1, teg2) + + def test_exception_group_summary(self): + eg_info = self.eg_info + eg = eg_info[1] + teg1 = traceback.TracebackExceptionGroup(*eg_info) + teg2 = traceback.TracebackExceptionGroup.from_exception(eg) + + excs = [ + eg.excs[0], + eg.excs[0].excs[0], + eg.excs[0].excs[1], + eg.excs[1] ] - self.assertEqual(exc.summary, expected_summary) - formatted_exception_only = list(exc.format_exception_only()) - - expected_exception_only = [ - 'exception_group.ExceptionGroup: eg2\n', - ' exception_group.ExceptionGroup: eg1\n', - ' ZeroDivisionError: division by zero\n', - ' ValueError: 42\n', - ' ValueError: 24\n' + expected_summary = [ + traceback.TracebackException(*eg_info, indent_level=0), + traceback.TracebackException.from_exception(excs[0], indent_level=1), + traceback.TracebackException.from_exception(excs[1], indent_level=2), + traceback.TracebackException.from_exception(excs[2], indent_level=2), + traceback.TracebackException.from_exception(excs[3], indent_level=1), ] + self.assertEqual(teg1.summary, expected_summary) + self.assertEqual(teg2.summary, expected_summary) - self.assertEqual(formatted_exception_only, expected_exception_only) + def test_exception_group_format_exception_only(self): + eg_info = self.eg_info + eg = eg_info[1] + teg = traceback.TracebackExceptionGroup(*eg_info) - formatted = ''.join(exc.format()).split('\n') - lno_f = f.__code__.co_firstlineno - lno_g = g.__code__.co_firstlineno + formatted = ''.join(teg.format_exception_only()).split('\n') + + expected = textwrap.dedent(f"""\ + exception_group.ExceptionGroup: eg2 + exception_group.ExceptionGroup: eg1 + ZeroDivisionError: division by zero + ValueError: 42 + ValueError: 24 + """).split('\n') + + self.assertEqual(formatted, expected) + + def test_exception_group_format(self): + eg_info = self.eg_info + eg = eg_info[1] + teg = traceback.TracebackExceptionGroup(*eg_info) + + formatted = ''.join(teg.format()).split('\n') + lno_f = self.lno_f + lno_g = self.lno_g expected = textwrap.dedent(f"""\ Traceback (most recent call last): - File "{__file__}", line {lno_g+21}, in test_exception_group - raise exception_group.ExceptionGroup( + File "{__file__}", line {lno_g+23}, in _get_exception_group + raise exception_group.ExceptionGroup("eg2", exc3, exc4) exception_group.ExceptionGroup: eg2 ------------------------------------------------------------ Traceback (most recent call last): - File "{__file__}", line {lno_g+13}, in test_exception_group - raise exception_group.ExceptionGroup( + File "{__file__}", line {lno_g+16}, in _get_exception_group + raise exception_group.ExceptionGroup("eg1", exc1, exc2) exception_group.ExceptionGroup: eg1 ------------------------------------------------------------ Traceback (most recent call last): - File "{__file__}", line {lno_g+6}, in test_exception_group + File "{__file__}", line {lno_g+9}, in _get_exception_group f() File "{__file__}", line {lno_f+1}, in f 1/0 ZeroDivisionError: division by zero ------------------------------------------------------------ Traceback (most recent call last): - File "{__file__}", line {lno_g+10}, in test_exception_group + File "{__file__}", line {lno_g+13}, in _get_exception_group g(42) File "{__file__}", line {lno_g+1}, in g raise ValueError(v) ValueError: 42 ------------------------------------------------------------ Traceback (most recent call last): - File "{__file__}", line {lno_g+18}, in test_exception_group + File "{__file__}", line {lno_g+20}, in _get_exception_group g(24) File "{__file__}", line {lno_g+1}, in g raise ValueError(v) @@ -1386,83 +1434,24 @@ def test_comparison(self): 1/0 except Exception: exc_info = sys.exc_info() - exc = traceback.TracebackExceptionGroup(*exc_info) - exc2 = traceback.TracebackExceptionGroup(*exc_info) + for _ in range(5): + try: + raise exc_info[1] + except: + exc_info = sys.exc_info() + exc = traceback.TracebackExceptionGroup(*exc_info) + exc2 = traceback.TracebackExceptionGroup(*exc_info) + exc3 = traceback.TracebackExceptionGroup(*exc_info, limit=300) + ne1 = traceback.TracebackExceptionGroup(*exc_info, indent_level=8) + ne2 = traceback.TracebackExceptionGroup(*exc_info, limit=3) self.assertIsNot(exc, exc2) self.assertEqual(exc, exc2) + self.assertEqual(exc, exc3) + self.assertNotEqual(exc, ne1) + self.assertNotEqual(exc, ne2) self.assertNotEqual(exc, object()) self.assertEqual(exc, ALWAYS_EQ) - def test_unhashable(self): - class UnhashableException(Exception): - def __eq__(self, other): - return True - - ex1 = UnhashableException('ex1') - ex2 = UnhashableException('ex2') - try: - raise ex2 from ex1 - except UnhashableException: - try: - raise ex1 - except UnhashableException: - exc_info = sys.exc_info() - exc = traceback.TracebackExceptionGroup(*exc_info) - formatted = list(exc.format()) - self.assertIn('UnhashableException: ex2\n', formatted[2]) - self.assertIn('UnhashableException: ex1\n', formatted[6]) - - def test_limit(self): - def recurse(n): - if n: - recurse(n-1) - else: - 1/0 - try: - recurse(10) - except Exception: - exc_info = sys.exc_info() - exc = traceback.TracebackExceptionGroup(*exc_info, limit=5) - expected_stack = traceback.StackSummary.extract( - traceback.walk_tb(exc_info[2]), limit=5) - self.assertEqual(expected_stack, exc.summary[0].stack) - - def test_lookup_lines(self): - linecache.clearcache() - e = Exception("uh oh") - c = test_code('/foo.py', 'method') - f = test_frame(c, None, None) - tb = test_tb(f, 6, None) - exc = traceback.TracebackExceptionGroup(Exception, e, tb, lookup_lines=False) - self.assertEqual({}, linecache.cache) - linecache.updatecache('/foo.py', globals()) - self.assertEqual(exc.summary[0].stack[0].line, "import sys") - - def test_locals(self): - linecache.updatecache('/foo.py', globals()) - e = Exception("uh oh") - c = test_code('/foo.py', 'method') - f = test_frame(c, globals(), {'something': 1, 'other': 'string'}) - tb = test_tb(f, 6, None) - exc = traceback.TracebackExceptionGroup( - Exception, e, tb, capture_locals=True) - self.assertEqual( - exc.summary[0].stack[0].locals, {'something': '1', 'other': "'string'"}) - - def test_no_locals(self): - linecache.updatecache('/foo.py', globals()) - e = Exception("uh oh") - c = test_code('/foo.py', 'method') - f = test_frame(c, globals(), {'something': 1}) - tb = test_tb(f, 6, None) - exc = traceback.TracebackExceptionGroup(Exception, e, tb) - self.assertEqual(exc.summary[0].stack[0].locals, None) - - def test_traceback_header(self): - # do not print a traceback header if exc_traceback is None - # see issue #24695 - exc = traceback.TracebackExceptionGroup(Exception, Exception("haven"), None) - self.assertEqual(list(exc.format()), ["Exception: haven\n"]) class MiscTest(unittest.TestCase): From dd23f00fc3c8322ba1b20866a53e2dd79c048e64 Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Wed, 2 Dec 2020 12:21:55 +0000 Subject: [PATCH 67/73] TracebackExceptionGroup is only for ExceptionGroups, and simper structure. indent_level implied by the recursive structure so removed. Removed render tests from test_exception_groups because that's tested in test_traceback --- Lib/test/test_exception_group.py | 47 ---------- Lib/test/test_traceback.py | 153 ++++++++++++++++--------------- Lib/traceback.py | 139 ++++++++++++++-------------- 3 files changed, 149 insertions(+), 190 deletions(-) diff --git a/Lib/test/test_exception_group.py b/Lib/test/test_exception_group.py index 06772221c0ae3d..69cdfc9768080d 100644 --- a/Lib/test/test_exception_group.py +++ b/Lib/test/test_exception_group.py @@ -190,53 +190,6 @@ def test_construction_nested(self): self.assertEqual(['newEG', 'newEG', 'newVE'], [f.name for f in tb]) -class ExceptionGroupRenderTests(ExceptionGroupTestUtils): - def test_simple(self): - bind = functools.partial - eg = self.newSimpleEG('hello world') - - expected = [ # (indent, exception) pairs - (0, eg), - (1, eg.excs[0]), - (1, eg.excs[1]), - (1, eg.excs[2]), - ] - - self.check_summary_format_and_render(eg, expected) - - def check_summary_format_and_render(self, eg, expected): - makeTE = traceback.TracebackException.from_exception - - summary = traceback.TracebackExceptionGroup.from_exception(eg).summary - self.assertEqual(len(expected), len(summary)) - self.assertEqual([e.indent_level for e in summary], [e[0] for e in expected]) - self.assertEqual([e for e in summary], - [makeTE(e[1], indent_level=e[0]) for e in expected]) - - # smoke test for traceback.TracebackExceptionGroup.format - format_output = list(traceback.TracebackExceptionGroup.from_exception(eg).format()) - self.assertIsInstance(format_output, list) - - def test_stack_summary_nested(self): - eg = self.newNestedEG(15) - - expected = [ # (indent, exception) pairs - (0, eg), - (1, eg.excs[0]), - (2, eg.excs[0].excs[0]), - (3, eg.excs[0].excs[0].excs[0]), - (3, eg.excs[0].excs[0].excs[1]), - (3, eg.excs[0].excs[0].excs[2]), - (2, eg.excs[0].excs[1]), - (3, eg.excs[0].excs[1].excs[0]), - (3, eg.excs[0].excs[1].excs[1]), - (3, eg.excs[0].excs[1].excs[2]), - (2, eg.excs[0].excs[2]), - (1, eg.excs[1]), - ] - self.check_summary_format_and_render(eg, expected) - - class ExceptionGroupSplitTests(ExceptionGroupTestUtils): def _split_exception_group(self, eg, types): """ Split an EG and do some sanity checks on the result """ diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index 0d6cf506cb39a5..259f527014b869 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -1266,22 +1266,48 @@ def test_traceback_header(self): exc = traceback.TracebackException(Exception, Exception("haven"), None) self.assertEqual(list(exc.format()), ["Exception: haven\n"]) + def test_ExceptionFormatter_factory(self): + def f(): + x = 12 + x/0 + + try: + f() + except: + exc_info = sys.exc_info() + + exc = exc_info[1] + + factory = traceback.ExceptionFormatter + direct = traceback.TracebackException + + self.assertEqual(factory.get(*exc_info), direct(*exc_info)) + self.assertEqual( + factory.from_exception(exc), direct.from_exception(exc)) -class TestTracebackGroupException(unittest.TestCase): + self.assertEqual( + factory.get(*exc_info, limit=1), direct(*exc_info, limit=1)) + self.assertEqual( + factory.from_exception(exc, limit=2), + direct.from_exception(exc, limit=2)) + + self.assertNotEqual( + factory.get(*exc_info, limit=1), direct(*exc_info, limit=2)) + self.assertNotEqual( + factory.from_exception(exc, limit=2), + direct.from_exception(exc, limit=1)) + + self.assertNotEqual( + factory.get(*exc_info, capture_locals=True), direct(*exc_info)) + self.assertNotEqual( + factory.from_exception(exc, capture_locals=True), + direct.from_exception(exc)) + +class TestTracebackExceptionGroup(unittest.TestCase): def setUp(self): super().setUp() - self.exc_info = self._get_exception() self.eg_info = self._get_exception_group() - def _get_exception(self): - def foo(): - 1/0 - try: - foo() - except Exception as e: - return sys.exc_info() - self.assertFalse('Exception Not Raised') - def _get_exception_group(self): def f(): 1/0 @@ -1310,34 +1336,20 @@ def g(v): except Exception as e: exc4 = e raise exception_group.ExceptionGroup("eg2", exc3, exc4) - except exception_group.ExceptionGroup as eg: + except exception_group.ExceptionGroup: return sys.exc_info() - self.assertFalse('Exception Not Raised') - - def test_single_exception_constructor(self): - exc_info = self.exc_info - teg1 = traceback.TracebackExceptionGroup(*exc_info) - exc1 = traceback.TracebackException(*exc_info) - self.assertEqual(teg1.summary, [exc1]) - - teg2 = traceback.TracebackExceptionGroup(*exc_info, indent_level=5) - exc2 = traceback.TracebackException(*exc_info, indent_level=5) - not_exc = traceback.TracebackException(*exc_info, indent_level=6) - self.assertEqual(teg2.summary, [exc2]) - self.assertNotEqual(teg2.summary, [not_exc]) - - def test_single_exception_from_exception(self): - exc_info = self.exc_info - teg1 = traceback.TracebackExceptionGroup.from_exception(exc_info[1]) - exc1 = traceback.TracebackException(*exc_info) - self.assertEqual(teg1.summary, [exc1]) - - teg2 = traceback.TracebackExceptionGroup.from_exception( - exc_info[1], indent_level=5) - exc2 = traceback.TracebackException(*exc_info, indent_level=5) - not_exc = traceback.TracebackException(*exc_info, indent_level=6) - self.assertEqual(teg2.summary, [exc2]) - self.assertNotEqual(teg2.summary, [not_exc]) + self.fail('Exception Not Raised') + + def test_single_exception_raises(self): + try: + 1/0 + except: + exc_info = sys.exc_info() + msg = "Expected an ExceptionGroup, got " + with self.assertRaisesRegex(ValueError, msg): + traceback.TracebackExceptionGroup(*exc_info) + with self.assertRaisesRegex(ValueError, msg): + traceback.TracebackExceptionGroup.from_exception(exc_info[1]) def test_exception_group_construction(self): eg_info = self.eg_info @@ -1346,35 +1358,9 @@ def test_exception_group_construction(self): self.assertIsNot(teg1, teg2) self.assertEqual(teg1, teg2) - def test_exception_group_summary(self): - eg_info = self.eg_info - eg = eg_info[1] - teg1 = traceback.TracebackExceptionGroup(*eg_info) - teg2 = traceback.TracebackExceptionGroup.from_exception(eg) - - excs = [ - eg.excs[0], - eg.excs[0].excs[0], - eg.excs[0].excs[1], - eg.excs[1] - ] - expected_summary = [ - traceback.TracebackException(*eg_info, indent_level=0), - traceback.TracebackException.from_exception(excs[0], indent_level=1), - traceback.TracebackException.from_exception(excs[1], indent_level=2), - traceback.TracebackException.from_exception(excs[2], indent_level=2), - traceback.TracebackException.from_exception(excs[3], indent_level=1), - ] - self.assertEqual(teg1.summary, expected_summary) - self.assertEqual(teg2.summary, expected_summary) - def test_exception_group_format_exception_only(self): - eg_info = self.eg_info - eg = eg_info[1] - teg = traceback.TracebackExceptionGroup(*eg_info) - + teg = traceback.TracebackExceptionGroup(*self.eg_info) formatted = ''.join(teg.format_exception_only()).split('\n') - expected = textwrap.dedent(f"""\ exception_group.ExceptionGroup: eg2 exception_group.ExceptionGroup: eg1 @@ -1386,9 +1372,7 @@ def test_exception_group_format_exception_only(self): self.assertEqual(formatted, expected) def test_exception_group_format(self): - eg_info = self.eg_info - eg = eg_info[1] - teg = traceback.TracebackExceptionGroup(*eg_info) + teg = traceback.TracebackExceptionGroup(*self.eg_info) formatted = ''.join(teg.format()).split('\n') lno_f = self.lno_f @@ -1431,8 +1415,8 @@ def test_exception_group_format(self): def test_comparison(self): try: - 1/0 - except Exception: + raise self.eg_info[1] + except exception_group.ExceptionGroup: exc_info = sys.exc_info() for _ in range(5): try: @@ -1442,16 +1426,37 @@ def test_comparison(self): exc = traceback.TracebackExceptionGroup(*exc_info) exc2 = traceback.TracebackExceptionGroup(*exc_info) exc3 = traceback.TracebackExceptionGroup(*exc_info, limit=300) - ne1 = traceback.TracebackExceptionGroup(*exc_info, indent_level=8) - ne2 = traceback.TracebackExceptionGroup(*exc_info, limit=3) + ne = traceback.TracebackExceptionGroup(*exc_info, limit=3) self.assertIsNot(exc, exc2) self.assertEqual(exc, exc2) self.assertEqual(exc, exc3) - self.assertNotEqual(exc, ne1) - self.assertNotEqual(exc, ne2) + self.assertNotEqual(exc, ne) self.assertNotEqual(exc, object()) self.assertEqual(exc, ALWAYS_EQ) + def test_ExceptionFormatter_factory(self): + exc_info = self.eg_info + + factory = traceback.ExceptionFormatter + direct = traceback.TracebackExceptionGroup + + self.assertEqual(factory.get(*exc_info), direct(*exc_info)) + self.assertEqual( + factory.from_exception(exc_info[1]), + direct.from_exception(exc_info[1])) + + self.assertEqual( + factory.get(*exc_info, limit=10), direct(*exc_info, limit=20)) + self.assertEqual( + factory.from_exception(exc_info[1], limit=10), + direct.from_exception(exc_info[1], limit=20)) + + self.assertNotEqual( + factory.get(*exc_info, capture_locals=True), direct(*exc_info)) + self.assertNotEqual( + factory.from_exception(exc_info[1], capture_locals=True), + direct.from_exception(exc_info[1])) + class MiscTest(unittest.TestCase): diff --git a/Lib/traceback.py b/Lib/traceback.py index 1aeecf06fb1a4d..7bd6797a4bc4e1 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -11,8 +11,9 @@ 'format_exception_only', 'format_list', 'format_stack', 'format_tb', 'print_exc', 'format_exc', 'print_exception', 'print_last', 'print_stack', 'print_tb', 'clear_frames', - 'FrameSummary', 'StackSummary', 'TracebackException', - 'TracebackExceptionGroup', 'walk_stack', 'walk_tb'] + 'ExceptionFormatter', 'FrameSummary', 'StackSummary', + 'TracebackException', 'TracebackExceptionGroup', + 'walk_stack', 'walk_tb'] # # Formatting and printing lists of traceback lines. @@ -73,6 +74,7 @@ def extract_tb(tb, limit=None): """ return StackSummary.extract(walk_tb(tb), limit=limit) + # # Exception formatting and output. # @@ -112,7 +114,7 @@ def print_exception(exc, /, value=_sentinel, tb=_sentinel, limit=None, \ value, tb = _parse_value_tb(exc, value, tb) if file is None: file = sys.stderr - for line in TracebackExceptionGroup( + for line in ExceptionFormatter.get( type(value), value, tb, limit=limit).format(chain=chain): print(line, file=file, end="") @@ -128,7 +130,7 @@ def format_exception(exc, /, value=_sentinel, tb=_sentinel, limit=None, \ printed as does print_exception(). """ value, tb = _parse_value_tb(exc, value, tb) - return list(TracebackExceptionGroup( + return list(ExceptionFormatter.get( type(value), value, tb, limit=limit).format(chain=chain)) @@ -148,7 +150,7 @@ def format_exception_only(exc, /, value=_sentinel): """ if value is _sentinel: value = exc - return list(TracebackExceptionGroup( + return list(ExceptionFormatter.get( type(value), value, None).format_exception_only()) @@ -478,7 +480,7 @@ class TracebackException: """ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None, - lookup_lines=True, capture_locals=False, _seen=None, indent_level=0): + lookup_lines=True, capture_locals=False, _seen=None): # NB: we need to accept exc_traceback, exc_value, exc_traceback to # permit backwards compat with the existing API, otherwise we # need stub thunk objects just to glue it together. @@ -533,7 +535,6 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None, self.msg = exc_value.msg if lookup_lines: self._load_lines() - self.indent_level = indent_level @classmethod def from_exception(cls, exc, *args, **kwargs): @@ -616,8 +617,8 @@ def format(self, *, chain=True): The return value is a generator of strings, each ending in a newline and some containing internal newlines. `print_exception(e)`, when `e` - is a single exception (rather than an ExceptionGroup), is essentially - a wrapper around this method which just prints the lines to a file. + is a single exception (rather than an ExceptionGroup), is a wrapper + around this method which just prints the lines to a file. The message indicating which exception occurred is always the last string in the output. @@ -637,7 +638,7 @@ def format(self, *, chain=True): class TracebackExceptionGroup: - """An exception or exception group ready for rendering. + """An exception group ready for rendering. We capture enough attributes from the original exception group to this intermediary form to ensure that no references are held, while still being @@ -647,88 +648,88 @@ class TracebackExceptionGroup: objects, or the constructor to create TracebackExceptionGroup instances from individual components. - - :attr:`summary` A list of TracebackException objects. For a single exception - the list has length 1, and for an ExceptionGroup it has an item for each exception - in the group (recursively), in DFS pre-order sequence: The first item represents - the given EG, the second represents the first exception in this EG's list, and - so on. + - :attr:`excs` A list of TracebackException objects, one for each exception + in the group. """ SEPARATOR_LINE = '-' * 60 + '\n' INDENT_SIZE = 3 - def __init__(self, exc_type, exc_value, exc_traceback, - indent_level=0, **kwargs): - self.summary = list( - self._gen_summary( - exc_type, exc_value, exc_traceback, - indent_level = indent_level, **kwargs)) - self._str = _some_str(exc_value) - - def _gen_summary(self, exc_type, exc_value, exc_traceback, - indent_level, **kwargs): - # recursively extract the sequence of (indent, TracebackException) - # pairs. If exc_value is a single exception, there is only one pair - # (with indent 0). If it is an exception group, we get one pair - # for each exception in it, showing the whole tree - yield TracebackException( - exc_type, exc_value, exc_traceback, - indent_level=indent_level, **kwargs) - if isinstance(exc_value, exception_group.ExceptionGroup): - for e in exc_value.excs: - yield from self._gen_summary( - type(e), e, e.__traceback__, indent_level + 1, **kwargs) + def __init__(self, exc_type, exc_value, exc_traceback, **kwargs): + if not isinstance(exc_value, exception_group.ExceptionGroup): + raise ValueError(f'Expected an ExceptionGroup, got {type(exc_value)}') + self.this = TracebackException( + exc_type, exc_value, exc_traceback, **kwargs) + self.excs = [ + ExceptionFormatter.from_exception(e) for e in exc_value.excs] - @classmethod - def from_exception(cls, exc, *args, **kwargs): - """Create a TracebackExceptionGroup from an exception.""" - return cls(type(exc), exc, exc.__traceback__, *args, **kwargs) + @staticmethod + def from_exception(exc, *args, **kwargs): + """Create a TracebackExceptionGroup from an exceptionGroup.""" + return TracebackExceptionGroup( + type(exc), exc, exc.__traceback__, *args, **kwargs) def format(self, *, chain=True): - """Format the exception or exception group. + """Format the exception group. - For an exception group, the shared part of the traceback is printed, - followed by a printout of each exception in the group, which is - expanded recursively. + The shared part of the traceback is emitted, followed by each + exception in the group, which is expanded recursively. If chain is false(y), *__cause__* and *__context__* will not be formatted. This is a generator of strings, each ending in a newline - and some containing internal newlines. `print_exception` is a wrapper - around this method which just prints the lines to a file. + and some containing internal newlines. `print_exception`, when called on + an ExceptionGroup, is a wrapper around this method which just prints the + lines to a file. """ # TODO: Add two args to bound - # (1) the depth of exceptions reported, and # (2) the number of exceptions reported per level - for te in self.summary: - yield from self._format( - te.format(chain=chain), - te.indent_level, - sep=self.SEPARATOR_LINE) + separator = self.SEPARATOR_LINE + yield from self.this.format(chain=chain) + for exc in self.excs: + yield from self._emit( + exc.format(chain=chain), sep=separator) def format_exception_only(self): - for te in self.summary: - yield from self._format( - te.format_exception_only(), - te.indent_level) - - def _format(self, text_gen, indent_level, sep=None): - if indent_level == 0: - yield from text_gen + yield from self.this.format_exception_only() + for exc in self.excs: + yield from self._emit(exc.format_exception_only()) + + def _emit(self, text_gen, sep=None): + text = ''.join(list(text_gen)) + indent_str = ' ' * self.INDENT_SIZE + if '\n' not in text: + yield indent_str + text else: - text = ''.join(list(text_gen)) - indent_str = ' ' * self.INDENT_SIZE * indent_level - if '\n' not in text: - yield indent_str + text - else: - if sep is not None: - yield indent_str + sep - yield textwrap.indent(text, indent_str) + if sep is not None: + yield indent_str + sep + yield textwrap.indent(text, indent_str) def __eq__(self, other): if isinstance(other, TracebackExceptionGroup): return self.__dict__ == other.__dict__ return NotImplemented - def __str__(self): - return self._str + +class ExceptionFormatter: + '''Factory functions to get the correct formatter for an exception + + Returns a TracebackException instance for a single exception, and a + TracebackExceptionGroup for an exception group. + ''' + + @staticmethod + def get(exc_type, exc_value, exc_traceback, **kwargs): + if isinstance(exc_value, exception_group.ExceptionGroup): + cls = TracebackExceptionGroup + else: + cls = TracebackException + return cls(exc_type, exc_value, exc_traceback, **kwargs) + + @staticmethod + def from_exception(exc, **kwargs): + if isinstance(exc, exception_group.ExceptionGroup): + return TracebackExceptionGroup.from_exception(exc, **kwargs) + else: + return TracebackException.from_exception(exc, **kwargs) \ No newline at end of file From e36d9b9e983f4e119ab09305336f0823cbd23dbf Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Mon, 7 Dec 2020 15:01:23 +0000 Subject: [PATCH 68/73] revert unintended changes to other tests --- Lib/test/test_traceback.py | 42 +++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index 259f527014b869..c8d5450d57622b 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -338,16 +338,16 @@ def f(): else: self.fail("no recursion occurred") - lno_f = f.__code__.co_firstlineno + lineno_f = f.__code__.co_firstlineno result_f = ( 'Traceback (most recent call last):\n' - f' File "{__file__}", line {lno_f+5}, in _check_recursive_traceback_display\n' + f' File "{__file__}", line {lineno_f+5}, in _check_recursive_traceback_display\n' ' f()\n' - f' File "{__file__}", line {lno_f+1}, in f\n' + f' File "{__file__}", line {lineno_f+1}, in f\n' ' f()\n' - f' File "{__file__}", line {lno_f+1}, in f\n' + f' File "{__file__}", line {lineno_f+1}, in f\n' ' f()\n' - f' File "{__file__}", line {lno_f+1}, in f\n' + f' File "{__file__}", line {lineno_f+1}, in f\n' ' f()\n' # XXX: The following line changes depending on whether the tests # are run through the interactive interpreter or with -m @@ -385,22 +385,22 @@ def g(count=10): else: self.fail("no value error was raised") - lno_g = g.__code__.co_firstlineno + lineno_g = g.__code__.co_firstlineno result_g = ( - f' File "{__file__}", line {lno_g+2}, in g\n' + f' File "{__file__}", line {lineno_g+2}, in g\n' ' return g(count-1)\n' - f' File "{__file__}", line {lno_g+2}, in g\n' + f' File "{__file__}", line {lineno_g+2}, in g\n' ' return g(count-1)\n' - f' File "{__file__}", line {lno_g+2}, in g\n' + f' File "{__file__}", line {lineno_g+2}, in g\n' ' return g(count-1)\n' ' [Previous line repeated 7 more times]\n' - f' File "{__file__}", line {lno_g+3}, in g\n' + f' File "{__file__}", line {lineno_g+3}, in g\n' ' raise ValueError\n' 'ValueError\n' ) tb_line = ( 'Traceback (most recent call last):\n' - f' File "{__file__}", line {lno_g+7}, in _check_recursive_traceback_display\n' + f' File "{__file__}", line {lineno_g+7}, in _check_recursive_traceback_display\n' ' g()\n' ) expected = (tb_line + result_g).splitlines() @@ -449,19 +449,19 @@ def h(count=10): else: self.fail("no error raised") result_g = ( - f' File "{__file__}", line {lno_g+2}, in g\n' + f' File "{__file__}", line {lineno_g+2}, in g\n' ' return g(count-1)\n' - f' File "{__file__}", line {lno_g+2}, in g\n' + f' File "{__file__}", line {lineno_g+2}, in g\n' ' return g(count-1)\n' - f' File "{__file__}", line {lno_g+2}, in g\n' + f' File "{__file__}", line {lineno_g+2}, in g\n' ' return g(count-1)\n' - f' File "{__file__}", line {lno_g+3}, in g\n' + f' File "{__file__}", line {lineno_g+3}, in g\n' ' raise ValueError\n' 'ValueError\n' ) tb_line = ( 'Traceback (most recent call last):\n' - f' File "{__file__}", line {lno_g+71}, in _check_recursive_traceback_display\n' + f' File "{__file__}", line {lineno_g+71}, in _check_recursive_traceback_display\n' ' g(traceback._RECURSIVE_CUTOFF)\n' ) expected = (tb_line + result_g).splitlines() @@ -477,20 +477,20 @@ def h(count=10): else: self.fail("no error raised") result_g = ( - f' File "{__file__}", line {lno_g+2}, in g\n' + f' File "{__file__}", line {lineno_g+2}, in g\n' ' return g(count-1)\n' - f' File "{__file__}", line {lno_g+2}, in g\n' + f' File "{__file__}", line {lineno_g+2}, in g\n' ' return g(count-1)\n' - f' File "{__file__}", line {lno_g+2}, in g\n' + f' File "{__file__}", line {lineno_g+2}, in g\n' ' return g(count-1)\n' ' [Previous line repeated 1 more time]\n' - f' File "{__file__}", line {lno_g+3}, in g\n' + f' File "{__file__}", line {lineno_g+3}, in g\n' ' raise ValueError\n' 'ValueError\n' ) tb_line = ( 'Traceback (most recent call last):\n' - f' File "{__file__}", line {lno_g+99}, in _check_recursive_traceback_display\n' + f' File "{__file__}", line {lineno_g+99}, in _check_recursive_traceback_display\n' ' g(traceback._RECURSIVE_CUTOFF + 1)\n' ) expected = (tb_line + result_g).splitlines() From 22a566fadd0ea4fbe88e2e3ee72569752a7dc13d Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Mon, 7 Dec 2020 15:28:06 +0000 Subject: [PATCH 69/73] add missing newline at end of file --- Lib/traceback.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/traceback.py b/Lib/traceback.py index 7bd6797a4bc4e1..ff0bccb6128412 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -732,4 +732,4 @@ def from_exception(exc, **kwargs): if isinstance(exc, exception_group.ExceptionGroup): return TracebackExceptionGroup.from_exception(exc, **kwargs) else: - return TracebackException.from_exception(exc, **kwargs) \ No newline at end of file + return TracebackException.from_exception(exc, **kwargs) From 0a6800c8b9ed483f055385b05c5de60ddb23937f Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Tue, 8 Dec 2020 14:07:16 +0000 Subject: [PATCH 70/73] minor test tidyup --- Lib/test/test_exception_group.py | 59 ++++++++++++-------------------- 1 file changed, 21 insertions(+), 38 deletions(-) diff --git a/Lib/test/test_exception_group.py b/Lib/test/test_exception_group.py index 69cdfc9768080d..e9e835945b47ae 100644 --- a/Lib/test/test_exception_group.py +++ b/Lib/test/test_exception_group.py @@ -33,8 +33,6 @@ def tracebackGroupSanityCheck(self, exc): for e in exc.excs: self.tracebackGroupSanityCheck(e) - -class ExceptionGroupTestUtils(ExceptionGroupTestBase): def newEG(self, message, raisers): excs = [] for r in raisers: @@ -117,19 +115,14 @@ def extract_traceback(self, exc, eg): return result -class ExceptionGroupConstructionTests(ExceptionGroupTestUtils): - def test_construction_simple(self): - # create a simple exception group and check that - # it is constructed as expected - bind = functools.partial +class ExceptionGroupBasicsTests(ExceptionGroupTestBase): + def test_simple_group(self): eg = self.newSimpleEG('simple EG') - self.assertEqual(len(eg.excs), 3) self.assertMatchesTemplate( eg, [ValueError(1), TypeError(int), ValueError(2)]) - # check iteration - self.assertEqual(list(eg), list(eg.excs)) + self.assertEqual(list(eg), list(eg.excs)) # check iteration # check message self.assertEqual(eg.message, 'simple EG') @@ -137,15 +130,12 @@ def test_construction_simple(self): # check tracebacks for e in eg: - expected = [ - 'newEG', - 'newEG', - 'new' + ''.join(filter(str.isupper, type(e).__name__)), - ] - etb = self.extract_traceback(e, eg) - self.assertEqual(expected, [f.name for f in etb]) - - def test_construction_nested(self): + fname = 'new' + ''.join(filter(str.isupper, type(e).__name__)) + self.assertEqual( + ['newEG', 'newEG', fname], + [f.name for f in self.extract_traceback(e, eg)]) + + def test_nested_group(self): eg = self.newNestedEG(5) self.assertMatchesTemplate( @@ -159,28 +149,21 @@ def test_construction_nested(self): ValueError(7) ]) - # check iteration - self.assertEqual(len(list(eg)), 8) - - # check tracebacks + self.assertEqual(len(list(eg)), 8) # check iteration self.tracebackGroupSanityCheck(eg) + # check tracebacks all_excs = list(eg) for e in all_excs[0:6]: - expected = [ - 'newEG', - 'newEG', - 'raiseExc', - 'newEG', - 'newEG', - 'raiseExc', - 'newEG', - 'newEG', - 'new' + ''.join(filter(str.isupper, type(e).__name__)), - ] - etb = self.extract_traceback(e, eg) - self.assertEqual(expected, [f.name for f in etb]) + fname = 'new' + ''.join(filter(str.isupper, type(e).__name__)) + self.assertEqual( + [ + 'newEG', 'newEG', 'raiseExc', + 'newEG', 'newEG', 'raiseExc', + 'newEG', 'newEG', fname, + ], + [f.name for f in self.extract_traceback(e, eg)]) tb = self.extract_traceback(all_excs[6], eg) self.assertEqual([ @@ -190,7 +173,7 @@ def test_construction_nested(self): self.assertEqual(['newEG', 'newEG', 'newVE'], [f.name for f in tb]) -class ExceptionGroupSplitTests(ExceptionGroupTestUtils): +class ExceptionGroupSplitTests(ExceptionGroupTestBase): def _split_exception_group(self, eg, types): """ Split an EG and do some sanity checks on the result """ self.assertIsInstance(eg, ExceptionGroup) @@ -280,7 +263,7 @@ def test_split_nested(self): self.assertMatchesTemplate(rest, valueErrors_template) -class ExceptionGroupCatchTests(ExceptionGroupTestUtils): +class ExceptionGroupCatchTests(ExceptionGroupTestBase): def setUp(self): super().setUp() From 701cd5dc6c37ba741ec859af889f1712879f9466 Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Tue, 8 Dec 2020 18:43:46 +0000 Subject: [PATCH 71/73] ExceptionGroup.project() now (1) checks the ExceptionGroup itself (and nested EGs) for matches and handles them correctly. (2) Returns None instead of empty EGs (3) is a staticmethod --- Lib/exception_group.py | 91 ++++++------ Lib/test/test_exception_group.py | 236 ++++++++++++++++--------------- 2 files changed, 168 insertions(+), 159 deletions(-) diff --git a/Lib/exception_group.py b/Lib/exception_group.py index 8731f975d7f13a..d5d9afd932c0e9 100644 --- a/Lib/exception_group.py +++ b/Lib/exception_group.py @@ -39,50 +39,55 @@ def __init__(self, message, *excs): # __traceback_group__ and __traceback__ combine to give the full path. self.__traceback_group__ = TracebackGroup(self.excs) - def project(self, condition, with_complement=False): + @staticmethod + def project(exc, condition, with_complement=False): """ Split an ExceptionGroup based on an exception predicate - returns a new ExceptionGroup, match, of the exceptions of self - for which condition returns True. If with_complement is True, + returns a new ExceptionGroup, match, of the exceptions of exc + for which condition returns true. If with_complement is true, returns another ExceptionGroup for the exception for which - condition returns False. - match and rest have the same nested structure as self, but empty + condition returns false. Note that condition is checked for + exc and nested ExceptionGroups as well, and if it returns true + then the whole ExceptionGroup is considered to be matched. + + match and rest have the same nested structure as exc, but empty sub-exceptions are not included. They have the same message, - __traceback__, __cause__ and __context__ fields as self. + __traceback__, __cause__ and __context__ fields as exc. condition: BaseException --> Boolean with_complement: Bool If True, construct also an EG of the non-matches """ - match = [] - rest = [] if with_complement else None - for e in self.excs: - if isinstance(e, ExceptionGroup): # recurse - e_match, e_rest = e.project( - condition, with_complement=with_complement) - if not e_match.is_empty(): + + if condition(exc): + return exc, None + elif not isinstance(exc, ExceptionGroup): + return None, exc if with_complement else None + else: + # recurse into ExceptionGroup + match_exc = rest_exc = None + match = [] + rest = [] if with_complement else None + for e in exc.excs: + e_match, e_rest = ExceptionGroup.project( + e, condition, with_complement=with_complement) + + if e_match is not None: match.append(e_match) - if with_complement and not e_rest.is_empty(): + if with_complement and e_rest is not None: rest.append(e_rest) - else: - if condition(e): - match.append(e) - elif with_complement: - rest.append(e) - - match_exc = ExceptionGroup( - self.message, *match).with_traceback(self.__traceback__) - - def copy_metadata(src, target): - target.__context__ = src.__context__ - target.__cause__ = src.__cause__ - - copy_metadata(self, match_exc) - if with_complement: - rest_exc = ExceptionGroup(self.message, *rest).with_traceback(self.__traceback__) - copy_metadata(self, rest_exc) - else: - rest_exc = None - return match_exc, rest_exc + + def copy_metadata(src, target): + target.__traceback__ = src.__traceback__ + target.__context__ = src.__context__ + target.__cause__ = src.__cause__ + + if match: + match_exc = ExceptionGroup(exc.message, *match) + copy_metadata(exc, match_exc) + if with_complement and rest: + rest_exc = ExceptionGroup(exc.message, *rest) + copy_metadata(exc, rest_exc) + return match_exc, rest_exc def split(self, type): """ Split an ExceptionGroup to extract exceptions of type E @@ -90,15 +95,14 @@ def split(self, type): type: An exception type """ return self.project( - lambda e: isinstance(e, type), - with_complement=True) + self, lambda e: isinstance(e, type), with_complement=True) def subgroup(self, keep): """ Split an ExceptionGroup to extract only exceptions in keep keep: List[BaseException] """ - match, _ = self.project(lambda e: e in keep) + match, _ = self.project(self, lambda e: e in keep) return match def __iter__(self): @@ -110,9 +114,6 @@ def __iter__(self): else: yield e - def is_empty(self): - return not any(self) - def __repr__(self): return f"ExceptionGroup({self.message}, {self.excs})" @@ -144,7 +145,7 @@ def __exit__(self, etype, exc, tb): if exc is not None and isinstance(exc, ExceptionGroup): match, rest = exc.split(self.types) - if match.is_empty(): + if match is None: # Let the interpreter reraise the exception return False @@ -160,15 +161,15 @@ def __exit__(self, etype, exc, tb): # reraise exc as is. return False - if handler_excs is None or handler_excs.is_empty(): - if rest.is_empty(): + if handler_excs is None: + if rest is None: # handled and swallowed all exceptions # do not raise anything. return True else: # raise the rest exceptions to_raise = rest - elif rest.is_empty(): + elif rest is None: to_raise = handler_excs # raise what handler returned else: # Merge handler's exceptions with rest @@ -179,7 +180,7 @@ def __exit__(self, etype, exc, tb): # to_add: new exceptions raised by handler to_add = handler_excs.subgroup( [e for e in handler_excs if e not in match]) - if not to_add.is_empty(): + if to_add is not None: to_raise = ExceptionGroup(exc.message, to_keep, to_add) else: to_raise = to_keep diff --git a/Lib/test/test_exception_group.py b/Lib/test/test_exception_group.py index e9e835945b47ae..acf411dd77f08b 100644 --- a/Lib/test/test_exception_group.py +++ b/Lib/test/test_exception_group.py @@ -7,6 +7,82 @@ from io import StringIO +def newEG(message, raisers, cls=ExceptionGroup): + excs = [] + for r in raisers: + try: + r() + except (Exception, ExceptionGroup) as e: + excs.append(e) + try: + raise cls(message, *excs) + except ExceptionGroup as e: + return e + +def newVE(v): + raise ValueError(v) + +def newTE(t): + raise TypeError(t) + +def newSimpleEG(msg=None): + bind = functools.partial + return newEG(msg, [bind(newVE, 1), bind(newTE, int), bind(newVE, 2)]) + +class MyExceptionGroup(ExceptionGroup): + pass + +def newNestedEG(arg, message=None): + bind = functools.partial + + def level1(i): + return newEG( + 'msg1', + [bind(newVE, i), bind(newTE, int), bind(newVE, i+1)]) + + def raiseExc(e): + raise e + + def level2(i): + return newEG( + 'msg2', + [bind(raiseExc, level1(i)), + bind(raiseExc, level1(i+1)), + bind(newVE, i+2), + ], + cls=MyExceptionGroup) + + def level3(i): + return newEG( + 'msg3', + [bind(raiseExc, level2(i+1)), bind(newVE, i+2)]) + + return level3(arg) + +def extract_traceback(exc, eg): + """ returns the traceback of a single exception + + If exc is in the exception group, return its + traceback as the concatenation of the outputs + of traceback.extract_tb() on each segment of + it traceback (one per each ExceptionGroup that + it belongs to). + """ + if exc not in eg: + return None + e = eg.subgroup([exc]) + result = None + while e is not None: + if isinstance(e, ExceptionGroup): + assert len(e.excs) == 1 and exc in e + r = traceback.extract_tb(e.__traceback__) + if result is not None: + result.extend(r) + else: + result = r + e = e.excs[0] if isinstance(e, ExceptionGroup) else None + return result + class ExceptionGroupTestBase(unittest.TestCase): def assertMatchesTemplate(self, exc, template): """ Assert that the exception matches the template """ @@ -33,91 +109,10 @@ def tracebackGroupSanityCheck(self, exc): for e in exc.excs: self.tracebackGroupSanityCheck(e) - def newEG(self, message, raisers): - excs = [] - for r in raisers: - try: - r() - except (Exception, ExceptionGroup) as e: - excs.append(e) - try: - raise ExceptionGroup(message, *excs) - except ExceptionGroup as e: - return e - - def newVE(self, v): - raise ValueError(v) - - def newTE(self, t): - raise TypeError(t) - - def newSimpleEG(self, message=None): - bind = functools.partial - return self.newEG( - message, - [bind(self.newVE, 1), - bind(self.newTE, int), - bind(self.newVE, 2), ]) - - def newNestedEG(self, arg, message=None): - bind = functools.partial - - def level1(i): - return self.newEG( - 'msg', - [bind(self.newVE, i), - bind(self.newTE, int), - bind(self.newVE, i+1), - ]) - - def raiseExc(e): - raise e - - def level2(i): - return self.newEG( - 'msg', - [bind(raiseExc, level1(i)), - bind(raiseExc, level1(i+1)), - bind(self.newVE, i+2), - ]) - - def level3(i): - return self.newEG( - 'msg', - [bind(raiseExc, level2(i+1)), - bind(self.newVE, i+2), - ]) - - return level3(arg) - - def extract_traceback(self, exc, eg): - """ returns the traceback of a single exception - - If exc is in the exception group, return its - traceback as the concatenation of the outputs - of traceback.extract_tb() on each segment of - it traceback (one per each ExceptionGroup that - it belongs to). - """ - if exc not in eg: - return None - e = eg.subgroup([exc]) - result = None - while e is not None: - if isinstance(e, ExceptionGroup): - assert len(e.excs) == 1 and exc in e - r = traceback.extract_tb(e.__traceback__) - if result is not None: - result.extend(r) - else: - result = r - e = e.excs[0] if isinstance(e, ExceptionGroup) else None - return result - class ExceptionGroupBasicsTests(ExceptionGroupTestBase): def test_simple_group(self): - eg = self.newSimpleEG('simple EG') + eg = newSimpleEG('simple EG') self.assertMatchesTemplate( eg, [ValueError(1), TypeError(int), ValueError(2)]) @@ -133,10 +128,10 @@ def test_simple_group(self): fname = 'new' + ''.join(filter(str.isupper, type(e).__name__)) self.assertEqual( ['newEG', 'newEG', fname], - [f.name for f in self.extract_traceback(e, eg)]) + [f.name for f in extract_traceback(e, eg)]) def test_nested_group(self): - eg = self.newNestedEG(5) + eg = newNestedEG(5) self.assertMatchesTemplate( eg, @@ -163,14 +158,15 @@ def test_nested_group(self): 'newEG', 'newEG', 'raiseExc', 'newEG', 'newEG', fname, ], - [f.name for f in self.extract_traceback(e, eg)]) + [f.name for f in extract_traceback(e, eg)]) - tb = self.extract_traceback(all_excs[6], eg) self.assertEqual([ 'newEG', 'newEG', 'raiseExc', 'newEG', 'newEG', 'newVE'], - [f.name for f in tb]) - tb = self.extract_traceback(all_excs[7], eg) - self.assertEqual(['newEG', 'newEG', 'newVE'], [f.name for f in tb]) + [f.name for f in extract_traceback(all_excs[6], eg)]) + + self.assertEqual( + ['newEG', 'newEG', 'newVE'], + [f.name for f in extract_traceback(all_excs[7], eg)]) class ExceptionGroupSplitTests(ExceptionGroupTestBase): @@ -182,35 +178,38 @@ def _split_exception_group(self, eg, types): match, rest = eg.split(types) - self.assertIsInstance(match, ExceptionGroup) - self.assertIsInstance(rest, ExceptionGroup) - self.assertEqual(len(list(all_excs)), - len(list(match)) + len(list(rest))) + if match is not None: + self.assertIsInstance(match, ExceptionGroup) + for e in match: + self.assertIsInstance(e, types) + + if rest is not None: + self.assertIsInstance(rest, ExceptionGroup) + for e in rest: + self.assertNotIsInstance(e, types) + + match_len = len(list(match)) if match is not None else 0 + rest_len = len(list(rest)) if rest is not None else 0 + self.assertEqual(len(list(all_excs)), match_len + rest_len) for e in all_excs: + # each exception is in eg and exactly one of match and rest self.assertIn(e, eg) - # every exception in all_excs is in eg and - # in exactly one of match and rest - self.assertNotEqual(e in match, e in rest) - - for e in match: - self.assertIsInstance(e, types) - for e in rest: - self.assertNotIsInstance(e, types) + self.assertNotEqual(match and e in match, rest and e in rest) for part in [match, rest]: - self.assertEqual(eg.message, part.message) - # check tracebacks - for e in part: - self.assertEqual( - self.extract_traceback(e, eg), - self.extract_traceback(e, part)) + if part is not None: + self.assertEqual(eg.message, part.message) + for e in part: + self.assertEqual( + extract_traceback(e, eg), + extract_traceback(e, part)) return match, rest def test_split_nested(self): try: - raise self.newNestedEG(25) + raise newNestedEG(25) except ExceptionGroup as e: eg = e @@ -241,16 +240,16 @@ def test_split_nested(self): # Match Nothing match, rest = self._split_exception_group(eg, SyntaxError) - self.assertTrue(match.is_empty()) + self.assertTrue(match is None) self.assertMatchesTemplate(rest, eg_template) # Match Everything match, rest = self._split_exception_group(eg, BaseException) self.assertMatchesTemplate(match, eg_template) - self.assertTrue(rest.is_empty()) + self.assertTrue(rest is None) match, rest = self._split_exception_group(eg, (ValueError, TypeError)) self.assertMatchesTemplate(match, eg_template) - self.assertTrue(rest.is_empty()) + self.assertTrue(rest is None) # Match ValueErrors match, rest = self._split_exception_group(eg, ValueError) @@ -262,13 +261,22 @@ def test_split_nested(self): self.assertMatchesTemplate(match, typeErrors_template) self.assertMatchesTemplate(rest, valueErrors_template) + # Match ExceptionGroup + match, rest = eg.split(ExceptionGroup) + self.assertIs(match, eg) + self.assertIsNone(rest) + + # Match MyExceptionGroup (ExceptionGroup subclass) + match, rest = eg.split(MyExceptionGroup) + self.assertMatchesTemplate(match, [eg_template[0]]) + self.assertMatchesTemplate(rest, [eg_template[1]]) class ExceptionGroupCatchTests(ExceptionGroupTestBase): def setUp(self): super().setUp() try: - raise self.newNestedEG(35) + raise newNestedEG(35) except ExceptionGroup as e: self.eg = e @@ -304,9 +312,9 @@ def checkMatch(self, exc, template, orig_eg): def f_data(f): return [f.name, f.lineno] - new = list(map(f_data, self.extract_traceback(e, exc))) + new = list(map(f_data, extract_traceback(e, exc))) if e in orig_eg: - old = list(map(f_data, self.extract_traceback(e, orig_eg))) + old = list(map(f_data, extract_traceback(e, orig_eg))) self.assertSequenceEqual(old, new[-len(old):]) class BaseHandler: From 26394d817b37e11a8eb5e3e19f29d97f13289fa1 Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Fri, 11 Dec 2020 11:53:12 +0000 Subject: [PATCH 72/73] remove the TracebackGroup class - we don't need it --- Lib/exception_group.py | 23 +---------------------- Lib/test/test_exception_group.py | 15 --------------- 2 files changed, 1 insertion(+), 37 deletions(-) diff --git a/Lib/exception_group.py b/Lib/exception_group.py index d5d9afd932c0e9..0fd11f278447da 100644 --- a/Lib/exception_group.py +++ b/Lib/exception_group.py @@ -4,29 +4,12 @@ import traceback -class TracebackGroup: - def __init__(self, excs): - self.tb_next_map = {} # exception id to tb - for e in excs: - if isinstance(e, ExceptionGroup): - for e_ in e.excs: - if isinstance(e_, ExceptionGroup): - ks = list(e_) - else: - ks = (e_,) - for k in ks: - self.tb_next_map[id(k)] = e_.__traceback__ - else: - self.tb_next_map[id(e)] = e.__traceback__ - - class ExceptionGroup(BaseException): def __init__(self, message, *excs): """ Construct a new ExceptionGroup + message: The exception Group's error message excs: sequence of exceptions - tb [optional]: the __traceback__ of this exception group. - Typically set when this ExceptionGroup is derived from another. """ assert message is None or isinstance(message, str) assert all(isinstance(e, BaseException) for e in excs) @@ -34,10 +17,6 @@ def __init__(self, message, *excs): self.message = message self.excs = excs super().__init__(self.message) - # self.__traceback__ is updated as usual, but self.__traceback_group__ - # is set when the exception group is created. - # __traceback_group__ and __traceback__ combine to give the full path. - self.__traceback_group__ = TracebackGroup(self.excs) @staticmethod def project(exc, condition, with_complement=False): diff --git a/Lib/test/test_exception_group.py b/Lib/test/test_exception_group.py index acf411dd77f08b..7d3d306601bd1b 100644 --- a/Lib/test/test_exception_group.py +++ b/Lib/test/test_exception_group.py @@ -96,19 +96,6 @@ def assertMatchesTemplate(self, exc, template): self.assertEqual(type(exc), type(template)) self.assertEqual(exc.args, template.args) - def tracebackGroupSanityCheck(self, exc): - if not isinstance(exc, ExceptionGroup): - return - - tbg = exc.__traceback_group__ - all_excs = list(exc) - self.assertEqual(len(tbg.tb_next_map), len(all_excs)) - self.assertEqual([i for i in tbg.tb_next_map], - [id(e) for e in exc]) - - for e in exc.excs: - self.tracebackGroupSanityCheck(e) - class ExceptionGroupBasicsTests(ExceptionGroupTestBase): def test_simple_group(self): @@ -146,8 +133,6 @@ def test_nested_group(self): self.assertEqual(len(list(eg)), 8) # check iteration - self.tracebackGroupSanityCheck(eg) - # check tracebacks all_excs = list(eg) for e in all_excs[0:6]: From 3e3c03f8159f3d291ce0e7ba4a11ae152420a853 Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Sun, 13 Dec 2020 13:23:48 +0000 Subject: [PATCH 73/73] remove TracebackGroup import in test --- Lib/test/test_exception_group.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_exception_group.py b/Lib/test/test_exception_group.py index 7d3d306601bd1b..96089a46823f23 100644 --- a/Lib/test/test_exception_group.py +++ b/Lib/test/test_exception_group.py @@ -3,7 +3,7 @@ import functools import traceback import unittest -from exception_group import ExceptionGroup, TracebackGroup +from exception_group import ExceptionGroup from io import StringIO