Skip to content

Commit

Permalink
Merge remote-tracking branch 'refs/remotes/nico/branches/py3.11-excep…
Browse files Browse the repository at this point in the history
…t-groups-impl' into branches/py3.11-except-groups-impl
  • Loading branch information
cfbolz committed Sep 24, 2024
2 parents e2e398b + 258eb95 commit 22d1dd9
Show file tree
Hide file tree
Showing 6 changed files with 319 additions and 15 deletions.
107 changes: 106 additions & 1 deletion extra_tests/test_exceptiongroups.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
from _pypy_exceptiongroups import BaseExceptionGroup, ExceptionGroup
from _pypy_exceptiongroups import \
BaseExceptionGroup, ExceptionGroup, \
_collect_eg_leafs, _exception_group_projection, \
_prep_reraise_star

import pytest


Expand Down Expand Up @@ -479,3 +483,104 @@ class MyEG(ExceptionGroup):
eg = MyEG("abc", [ValueError(), TypeError()])
eg2 = eg.derive([ValueError()])
assert type(eg2) is ExceptionGroup


### helper function tests

def test_eg_leafs_basic():
t1, v1 = TypeError(), ValueError()
exceptions = [t1, v1]
message = "42"
excgroup = ExceptionGroup(message, exceptions)
resultset = set()
_collect_eg_leafs(excgroup, resultset)
assert {t1, v1} == resultset

def test_eg_leafs_null():
resultset = set()
_collect_eg_leafs(None, resultset)
assert not resultset

def test_eg_leafs_nogroup():
exc = TypeError()
resultset = set()
_collect_eg_leafs(exc, resultset)
assert resultset == {exc}

def test_eg_leafs_recursive():
# TODO: fix
val1 = ValueError(1)
typ1 = TypeError()
val2 = ValueError(2)
val3 = ValueError(3)
typ2 = TypeError()
key1 = KeyError()
div1 = ZeroDivisionError()
eg = ExceptionGroup("abc", [key1, val1, ExceptionGroup("def", [val2, val3, typ2, div1]), typ1])
resultset = set()
collected = _collect_eg_leafs(eg, resultset)
assert len(resultset) == 7
for e in [val1, typ1, val2, val3, typ2, key1, div1]:
assert e in resultset

def test_exception_group_projection_basic():
val1 = ValueError(1)
typ1 = TypeError()
val2 = ValueError(2)
val3 = ValueError(3)
typ2 = TypeError()
key1 = KeyError()
div1 = ZeroDivisionError()
eg = ExceptionGroup("abc", [key1, val1, ExceptionGroup("def", [val2, val3, typ2, div1]), typ1])
keep1 = ExceptionGroup("meep", [key1, typ1])
keep2 = ExceptionGroup("moop", [val2, ExceptionGroup("doop", [val3])])
result = _exception_group_projection(eg, [keep1, keep2])
assert repr(result) == \
"ExceptionGroup('abc', [KeyError(), ExceptionGroup('def', [ValueError(2), ValueError(3)]), TypeError()])"

def test_exception_group_projection_duplicated_in_keep():
val1 = ValueError(1)
typ1 = TypeError()
val2 = ValueError(2)
val3 = ValueError(3)
typ2 = TypeError()
key1 = KeyError()
div1 = ZeroDivisionError()
eg = ExceptionGroup("abc", [key1, val1, ExceptionGroup("def", [val2, val3, typ2, div1]), typ1])
keep1 = ExceptionGroup("meep", [key1, typ1, val2])
keep2 = ExceptionGroup("moop", [val2, ExceptionGroup("doop", [key1, val3])])
result = _exception_group_projection(eg, [keep1, keep2])
assert repr(result) == \
"ExceptionGroup('abc', [KeyError(), ExceptionGroup('def', [ValueError(2), ValueError(3)]), TypeError()])"

# TODO: Duplicates in eg?


# _prep_reraise_star tests

def test_prep_reraise_star_simple():
assert _prep_reraise_star(TypeError(), [None]) is None
assert _prep_reraise_star(ExceptionGroup('abc', [ValueError(), TypeError()]), [None]) is None

value = ValueError()
res = _prep_reraise_star(ExceptionGroup('abc', [value, TypeError()]), [ExceptionGroup('abc', [value])])
assert repr(res) == "ExceptionGroup('abc', [ValueError()])"
assert res.exceptions[0] is value

def test_prep_reraise_exception_happens_in_except_star():
value = ValueError()
full_eg = ExceptionGroup('abc', [value, TypeError()])
value_eg = ExceptionGroup('abc', [value])
try:
raise Exception
except Exception as e:
tb1 = e.__traceback__
try:
raise Exception
except Exception as e:
tb2 = e.__traceback__
full_eg.__traceback__ = tb1
value_eg.__traceback__ = tb1
zerodiv = ZeroDivisionError('division by zero')
zerodiv.__traceback__ = tb2
assert repr(_prep_reraise_star(full_eg, [zerodiv, value_eg])) == "ExceptionGroup('', [ZeroDivisionError('division by zero'), ExceptionGroup('abc', [ValueError()])])"
95 changes: 93 additions & 2 deletions lib_pypy/_pypy_exceptiongroups.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,10 @@ def __str__(self):
def __repr__(self):
return f"{self.__class__.__name__}({self.message!r}, {list(self._exceptions)!r})"

class ExceptionGroup(BaseExceptionGroup, Exception):
pass


def _derive_and_copy_attrs(self, excs):
eg = self.derive(excs)
if hasattr(self, "__notes__"):
Expand All @@ -119,8 +123,95 @@ def _derive_and_copy_attrs(self, excs):
eg.__traceback__ = self.__traceback__
return eg

class ExceptionGroup(BaseExceptionGroup, Exception):
pass
def _exception_group_projection(eg, keep_list):
"""
From CPython:
/* This function is used by the interpreter to construct reraised
* exception groups. It takes an exception group eg and a list
* of exception groups keep and returns the sub-exception group
* of eg which contains all leaf exceptions that are contained
* in any exception group in keep.
*/
"""
# TODO: muss es nicht eigentlich anders herum sein
# (return rest instead of match)?
assert isinstance(eg, BaseExceptionGroup)
assert isinstance(keep_list, list)

resultset = set()
for keep in keep_list:
_collect_eg_leafs(keep, resultset)

# TODO: maybe don't construct rest eg
split_match, _ = eg.split(lambda element: element in resultset)

return split_match

def _collect_eg_leafs(eg_or_exc, resultset):
if eg_or_exc == None:
# empty exception groups appear as a result
# of matches (split, subgroup) and thus are valid
pass
elif isinstance(eg_or_exc, BaseExceptionGroup):
# recursively collect children of eg
for subexc in eg_or_exc._exceptions:
_collect_eg_leafs(subexc, resultset)
elif isinstance(eg_or_exc, BaseException):
# we have a single exception (not a group),
# return a singleton list containing the exc
resultset.add(eg_or_exc)
else:
raise TypeError(f"expected BaseException, got {type(eg_or_exc)}")

def _prep_reraise_star(orig, exc_list):
assert isinstance(orig, BaseException)
assert isinstance(exc_list, list)

# TODO: test this:
if len(exc_list) < 1:
return None

for exc in exc_list: assert isinstance(exc, BaseException) or exc is None

if not isinstance(orig, BaseExceptionGroup):
# a naked exception was caught and wrapped. Only one except* clause
# could have executed,so there is at most one exception to raise.
assert len(exc_list) == 1 or (len(exc_list) == 2 and exc_list[1] is None)
return exc_list[0]

raised_list = []
reraised_list = []
for exc in exc_list:
if exc != None:
if _is_same_exception_metadata(exc, orig):
reraised_list.append(exc)
else:
raised_list.append(exc)

reraised_eg = _exception_group_projection(orig, reraised_list)
if reraised_eg is not None:
assert _is_same_exception_metadata(reraised_eg, orig)

if not raised_list:
return reraised_eg
if reraised_eg is not None:
raised_list.append(reraised_eg)
if len(raised_list) == 1:
return raised_list[0]
return ExceptionGroup("", raised_list)


_SENTINEL = object()

def _is_same_exception_metadata(exc1, exc2):
# TODO: Exception or BaseException?
assert isinstance(exc1, Exception)
assert isinstance(exc2, Exception)

return (getattr(exc1, '__notes__', _SENTINEL) == getattr(exc2, '__notes__', _SENTINEL) and
exc1.__traceback__ == exc2.__traceback__ and
exc1.__cause__ == exc2.__cause__ and
exc1.__context__ == exc2.__context__)

def get_condition_filter(condition):
if isinstance(condition, type) and issubclass(condition, BaseException):
Expand Down
38 changes: 32 additions & 6 deletions pypy/interpreter/astcompiler/codegen.py
Original file line number Diff line number Diff line change
Expand Up @@ -1083,17 +1083,43 @@ def visit_TryStar(self, tr):
self.load_const(self.space.w_None)
self.emit_op(ops.IS_OP)
self.emit_jump(ops.POP_JUMP_IF_TRUE, pop_next_except)
if handler.name is None:
self.emit_op(ops.POP_TOP)
else:
assert 0, "implement me"

exception_in_exc_body = self.new_block() # R1 in comment above
cleanup_body = self.new_block()
if handler.name is not None:
self.name_op(handler.name, ast.Store, handler)
else:
self.emit_op(ops.POP_TOP)
## generate the equivalent of:
##
## try:
## < body >
## except* type as name:
## try:
## < body >
## name = None
## del name
## except:
## name = None
## del name
## continue with except* handling
#
self.emit_jump(ops.SETUP_EXCEPT, exception_in_exc_body)
self._visit_body(handler.body)
self.emit_op(ops.POP_BLOCK) # XXX missing in CPython comment
if handler.name:
self.load_const(self.space.w_None)
self.name_op(handler.name, ast.Store, handler)
self.name_op(handler.name, ast.Del, handler)
self.emit_jump(ops.JUMP_FORWARD, next_except_with_nop)

self.use_next_block(exception_in_exc_body)
if handler.name:
self.load_const(self.space.w_None)
self.name_op(handler.name, ast.Store, handler)
self.name_op(handler.name, ast.Del, handler)

self.emit_op(ops.POP_TOP) # get rid of type
self.emit_op_arg(ops.LIST_APPEND, 3)
self.emit_op(ops.POP_TOP)
self.emit_jump(ops.JUMP_FORWARD, next_except)
Expand Down Expand Up @@ -1122,9 +1148,9 @@ def visit_TryStar(self, tr):
self.update_position(handler)
self.pop_frame_block(F_EXCEPTION_HANDLER, None)
# pypy difference: get rid of exception # XXX is this correct for groups?
self.emit_op(ops.ROT_TWO)
self.emit_op(ops.POP_TOP)
self.emit_op(ops.POP_TOP)
self.emit_op(ops.RERAISE) # reraise uses the SApplicationException
self.emit_op(ops.RERAISE)
self.use_next_block(otherwise)
self._visit_body(tr.orelse)
self.use_next_block(end)
Expand Down
77 changes: 77 additions & 0 deletions pypy/interpreter/astcompiler/test/apptest_exceptiongroup.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from pytest import raises

def test_simple():
try:
raise TypeError()
Expand All @@ -6,3 +8,78 @@ def test_simple():
except* ValueError:
a = 2
assert a == 1

def test_both_excepts_run():
l = []
try:
raise ExceptionGroup('abc', [ValueError(), TypeError()])
except* TypeError:
l.append(1)
except* ValueError:
l.append(2)
print(l)
assert l == [1, 2]

def raises_one():
try:
raise ExceptionGroup('abc', [ValueError(), TypeError()])
except* TypeError:
pass

def test_reraise():
a = 1
try:
raises_one()
except* ValueError:
a = 0
assert a == 0 # and in particular, we reach this line

def error_in_handler():
try:
raise ExceptionGroup('abc', [ValueError(), TypeError()])
except* TypeError:
1 / 0

def test_error_in_exception_handler():
a = 1
try:
error_in_handler()
except ExceptionGroup as e:
assert repr(e) == "ExceptionGroup('', [ZeroDivisionError('division by zero'), ExceptionGroup('abc', [ValueError()])])"
# TODO what's wrong with the context?
#assert repr(e.exceptions[0].__context__) == "ExceptionGroup('abc', [TypeError()])"
else:
assert 0, "an ExceptionGroup should be raised"

def test_name_except_star():
l = []
value = ValueError()
typ = TypeError()
try:
raise ExceptionGroup('abc', [value, typ])
except* TypeError as e1:
assert e1.exceptions[0] is typ
l.append(1)
except* ValueError as e2:
assert e2.exceptions[0] is value
l.append(2)
print(l)
assert l == [1, 2]
with raises(UnboundLocalError):
e1
with raises(UnboundLocalError):
e2

def test_try_star_name_raise_in_except_handler():
l = []
value = ValueError()
typ = TypeError()
try:
try:
raise ExceptionGroup('abc', [value, typ])
except* TypeError as e1:
1 / 0
except Exception as e:
assert "ZeroDivisionError" in repr(e)
with raises(UnboundLocalError):
e1
2 changes: 1 addition & 1 deletion pypy/interpreter/cpycode.c
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ _PyExc_PrepReraiseStar(PyObject *orig, PyObject *excs)
PyObject *append_list = is_reraise ? reraised_list : raised_list;
if (PyList_Append(append_list, e) < 0)
{
goto done;
goto done; // TODO Was ist hiermit?
}
}

Expand Down
Loading

0 comments on commit 22d1dd9

Please sign in to comment.