Skip to content

Commit b47311c

Browse files
authored
[mypyc] Support "raise ... from ..." properly (#22148)
The `from` clause of a `raise` statement was silently ignored in compiled code, so `__cause__` stayed `None` and `__suppress_context__` stayed `False`. Also make error checking in raise statements a bit more compatible with CPython. Fixes mypyc/mypyc#1224.
1 parent bbd7904 commit b47311c

6 files changed

Lines changed: 215 additions & 9 deletions

File tree

‎mypyc/irbuild/statement.py‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@
121121
keep_propagating_op,
122122
no_err_occurred_op,
123123
propagate_if_error_op,
124+
raise_exception_from_op,
124125
raise_exception_op,
125126
reraise_exception_op,
126127
restore_exc_info_op,
@@ -679,7 +680,16 @@ def transform_raise_stmt(builder: IRBuilder, s: RaiseStmt) -> None:
679680
return
680681

681682
exc = builder.accept(s.expr)
682-
builder.call_c(raise_exception_op, [exc], s.line)
683+
if s.from_expr is not None:
684+
if isinstance(exc, Register):
685+
# Evaluating the cause may reassign the variable holding the exception.
686+
temp = Register(exc.type)
687+
builder.assign(temp, exc, s.line)
688+
exc = temp
689+
cause = builder.accept(s.from_expr)
690+
builder.call_c(raise_exception_from_op, [exc, cause], s.line)
691+
else:
692+
builder.call_c(raise_exception_op, [exc], s.line)
683693
builder.add(Unreachable())
684694

685695

‎mypyc/lib-rt/CPy.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -979,6 +979,7 @@ static inline bool CPy_KeepPropagating(void) {
979979
#define CPy_ExcState() PyThreadState_GET()->exc_info
980980

981981
void CPy_Raise(PyObject *exc);
982+
void CPy_RaiseFrom(PyObject *exc, PyObject *cause);
982983
void CPy_Reraise(void);
983984
void CPyErr_SetObjectAndTraceback(PyObject *type, PyObject *value, PyObject *traceback);
984985
tuple_T3OOO CPy_CatchError(void);

‎mypyc/lib-rt/exc_ops.c‎

Lines changed: 58 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,67 @@
77
#include <Python.h>
88
#include "CPy.h"
99

10-
void CPy_Raise(PyObject *exc) {
11-
if (PyObject_IsInstance(exc, (PyObject *)&PyType_Type)) {
12-
PyObject *obj = PyObject_CallNoArgs(exc);
13-
if (!obj)
10+
// Return a new exception instance, or NULL with an error set.
11+
static PyObject *instantiate_exception(PyObject *type) {
12+
PyObject *value = PyObject_CallNoArgs(type);
13+
if (!value)
14+
return NULL;
15+
if (!PyExceptionInstance_Check(value)) {
16+
PyErr_Format(PyExc_TypeError,
17+
"calling %R should have returned an instance of "
18+
"BaseException, not %R", type, Py_TYPE(value));
19+
Py_DECREF(value);
20+
return NULL;
21+
}
22+
return value;
23+
}
24+
25+
// A NULL cause means no 'from' clause; Py_None means 'from None'.
26+
static void raise_exception(PyObject *exc, PyObject *cause) {
27+
PyObject *type;
28+
PyObject *value;
29+
if (PyExceptionClass_Check(exc)) {
30+
type = exc;
31+
value = instantiate_exception(exc);
32+
if (!value)
1433
return;
15-
PyErr_SetObject(exc, obj);
16-
Py_DECREF(obj);
34+
} else if (PyExceptionInstance_Check(exc)) {
35+
type = (PyObject *)Py_TYPE(exc);
36+
value = Py_NewRef(exc);
1737
} else {
18-
PyErr_SetObject((PyObject *)Py_TYPE(exc), exc);
38+
PyErr_SetString(PyExc_TypeError, "exceptions must derive from BaseException");
39+
return;
40+
}
41+
42+
if (cause != NULL) {
43+
PyObject *fixed_cause;
44+
if (PyExceptionClass_Check(cause)) {
45+
fixed_cause = instantiate_exception(cause);
46+
if (!fixed_cause)
47+
goto fail;
48+
} else if (PyExceptionInstance_Check(cause)) {
49+
fixed_cause = Py_NewRef(cause);
50+
} else if (Py_IsNone(cause)) {
51+
fixed_cause = NULL;
52+
} else {
53+
PyErr_SetString(PyExc_TypeError, "exception causes must derive from BaseException");
54+
goto fail;
55+
}
56+
// This steals the reference to the cause and sets __suppress_context__
57+
PyException_SetCause(value, fixed_cause);
1958
}
59+
// Normalize against the original class, even if __new__ returned an unrelated exception.
60+
PyErr_SetObject(type, value);
61+
fail:
62+
Py_DECREF(value);
63+
}
64+
65+
void CPy_Raise(PyObject *exc) {
66+
raise_exception(exc, NULL);
67+
}
68+
69+
void CPy_RaiseFrom(PyObject *exc, PyObject *cause) {
70+
raise_exception(exc, cause);
2071
}
2172

2273
void CPy_Reraise(void) {

‎mypyc/primitives/exc_ops.py‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@
1515
error_kind=ERR_ALWAYS,
1616
)
1717

18+
# Like raise_exception_op, but also set the cause (raise <exc> from <cause>).
19+
raise_exception_from_op = custom_op(
20+
arg_types=[object_rprimitive, object_rprimitive],
21+
return_type=void_rtype,
22+
c_function_name="CPy_RaiseFrom",
23+
error_kind=ERR_ALWAYS,
24+
)
25+
1826
# Raise StopIteration exception with the specified value (which can be NULL).
1927
set_stop_iteration_value = custom_op(
2028
arg_types=[object_rprimitive],

‎mypyc/test-data/fixtures/ir.py‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -351,7 +351,10 @@ def fget(self) -> Any: ...
351351
def fset(self, value: Any) -> None: ...
352352
def fdel(self) -> None: ...
353353

354-
class BaseException: pass
354+
class BaseException:
355+
__cause__: Optional[BaseException]
356+
__context__: Optional[BaseException]
357+
__suppress_context__: bool
355358

356359
class Exception(BaseException):
357360
def __init__(self, message: Optional[str] = None) -> None: pass

‎mypyc/test-data/run-exceptions.test‎

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -532,3 +532,136 @@ Traceback (most recent call last):
532532
File "native.py", line 6, in <module>
533533
f(y)
534534
TypeError: int object expected; got str
535+
536+
[case testRaiseFrom]
537+
from typing import Any
538+
539+
from exceptions_helper import OtherException
540+
from testutil import assertRaises
541+
542+
class Inner(Exception):
543+
pass
544+
545+
class Outer(Exception):
546+
pass
547+
548+
def raise_from(exc: Any, cause: Any) -> None:
549+
try:
550+
raise Inner("inner")
551+
except Inner:
552+
raise exc from cause
553+
554+
def test_raise_from_instance() -> None:
555+
cause = ValueError("cause")
556+
try:
557+
raise_from(Outer("outer"), cause)
558+
except Outer as e:
559+
assert e.__cause__ is cause
560+
assert e.__suppress_context__
561+
assert isinstance(e.__context__, Inner)
562+
else:
563+
assert False
564+
565+
def test_raise_from_class() -> None:
566+
try:
567+
raise_from(Outer, ValueError)
568+
except Outer as e:
569+
assert type(e.__cause__) is ValueError
570+
assert e.__suppress_context__
571+
else:
572+
assert False
573+
574+
def test_raise_from_none() -> None:
575+
try:
576+
raise_from(Outer("outer"), None)
577+
except Outer as e:
578+
assert e.__cause__ is None
579+
assert e.__suppress_context__
580+
assert isinstance(e.__context__, Inner)
581+
else:
582+
assert False
583+
584+
def test_raise_from_invalid_cause() -> None:
585+
with assertRaises(TypeError, "exception causes must derive from BaseException"):
586+
raise_from(Outer("outer"), 1)
587+
588+
def test_raise_from_invalid_exception() -> None:
589+
with assertRaises(TypeError, "exceptions must derive from BaseException"):
590+
raise_from(1, ValueError())
591+
592+
def test_raise_from_in_except() -> None:
593+
try:
594+
try:
595+
raise Inner("inner")
596+
except Inner as exc:
597+
raise Outer("outer") from exc
598+
except Outer as e:
599+
assert isinstance(e.__cause__, Inner)
600+
assert e.__cause__ is e.__context__
601+
else:
602+
assert False
603+
604+
def test_raise_from_reassigned_exception() -> None:
605+
original = ValueError("original")
606+
cause = TypeError("cause")
607+
exc: Exception = original
608+
try:
609+
raise exc from (exc := cause)
610+
except ValueError as e:
611+
assert e is original
612+
assert e.__cause__ is cause
613+
assert exc is cause
614+
else:
615+
assert False
616+
617+
def test_raise_from_class_returning_unrelated_exception() -> None:
618+
# Normalization calls OtherException again with the ValueError as an argument.
619+
with assertRaises(TypeError):
620+
raise_from(OtherException, None)
621+
622+
def raise_plain(exc: Any) -> None:
623+
raise exc
624+
625+
def test_raise_plain_preserves_cause() -> None:
626+
cause = ValueError("cause")
627+
exc = Outer("outer")
628+
try:
629+
raise_from(exc, cause)
630+
except Outer:
631+
pass
632+
try:
633+
raise_plain(exc)
634+
except Outer as e:
635+
assert e is exc
636+
assert e.__cause__ is cause
637+
assert e.__suppress_context__
638+
else:
639+
assert False
640+
641+
def test_raise_plain_does_not_suppress_context() -> None:
642+
try:
643+
try:
644+
raise Inner("inner")
645+
except Inner:
646+
raise_plain(Outer("outer"))
647+
except Outer as e:
648+
assert e.__cause__ is None
649+
assert not e.__suppress_context__
650+
assert isinstance(e.__context__, Inner)
651+
else:
652+
assert False
653+
654+
def test_raise_plain_invalid_exception() -> None:
655+
with assertRaises(TypeError, "exceptions must derive from BaseException"):
656+
raise_plain(1)
657+
658+
def test_raise_plain_class_returning_unrelated_exception() -> None:
659+
with assertRaises(TypeError):
660+
raise_plain(OtherException)
661+
662+
[file exceptions_helper.py]
663+
from typing import Any
664+
665+
class OtherException(Exception):
666+
def __new__(cls) -> Any:
667+
return ValueError("other")

0 commit comments

Comments
 (0)