Skip to content

Commit 766b369

Browse files
committed
runtime: exception chaining — expose __cause__/__context__/__suppress_context__
`raise X from Y` already stored the cause on the exception (RaiseVarargs::execute), but BaseException exposed none of the chaining attributes, so __cause__ was unreadable. - Add context()/set_context() and suppress_context()/set_suppress_context() accessors on BaseException (m_context/m_cause/m_suppress_context already existed and are GC-visited). - Register __cause__, __context__ and __suppress_context__ as read/write properties. Setting __cause__ also sets __suppress_context__ = True, and __suppress_context__ coerces via truthiness, matching the data model. - `raise X from Y` now also sets __suppress_context__ = True (and `from None` keeps the suppression with a None cause). Implicit __context__ chaining (auto-setting the new exception's context to the one being handled) is intentionally left out: the frame exception stack is not reliably popped — internally-consumed StopIterations linger (the same pre-existing bug that makes a bare `raise` outside a handler re-raise a stale exception instead of erroring), so reading it would attach a spurious context.
1 parent dd10eef commit 766b369

4 files changed

Lines changed: 114 additions & 2 deletions

File tree

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# Regression: explicit exception chaining via `raise X from Y` exposes
2+
# __cause__/__context__/__suppress_context__.
3+
#
4+
# NB: implicit __context__ chaining (auto-set to the exception being handled) is
5+
# deliberately NOT done yet — the frame exception stack isn't reliably popped
6+
# (internally-consumed StopIterations linger; the same bug breaks bare `raise`
7+
# outside a handler), so the attribute is exposed and settable but defaults to
8+
# None rather than being populated from that unreliable state.
9+
10+
11+
# `raise X from Y` sets __cause__ to the instance and suppresses context.
12+
try:
13+
try:
14+
raise ValueError("inner")
15+
except ValueError as e:
16+
raise KeyError("outer") from e
17+
except KeyError as k:
18+
assert isinstance(k.__cause__, ValueError), k.__cause__
19+
assert str(k.__cause__) == "inner", str(k.__cause__)
20+
assert k.__suppress_context__ is True, k.__suppress_context__
21+
22+
23+
# `raise X from None` -> cause None, still suppressed.
24+
try:
25+
raise KeyError("k") from None
26+
except KeyError as k:
27+
assert k.__cause__ is None, k.__cause__
28+
assert k.__suppress_context__ is True, k.__suppress_context__
29+
30+
31+
# Plain exception: all three default cleanly.
32+
try:
33+
raise ValueError("plain")
34+
except ValueError as e:
35+
assert e.__cause__ is None, e.__cause__
36+
assert e.__context__ is None, e.__context__
37+
assert e.__suppress_context__ is False, e.__suppress_context__
38+
39+
40+
# The chaining attributes are writable; setting __cause__ also suppresses context.
41+
try:
42+
raise ValueError("x")
43+
except ValueError as e:
44+
ctx = RuntimeError("ctx")
45+
e.__context__ = ctx
46+
assert e.__context__ is ctx
47+
e.__cause__ = ctx
48+
assert e.__cause__ is ctx
49+
assert e.__suppress_context__ is True
50+
e.__suppress_context__ = False
51+
assert e.__suppress_context__ is False
52+
53+
54+
# `from` works with a freshly-constructed cause too.
55+
try:
56+
raise ValueError("v") from TypeError("t")
57+
except ValueError as e:
58+
assert isinstance(e.__cause__, TypeError), e.__cause__
59+
assert str(e.__cause__) == "t", str(e.__cause__)
60+
61+
62+
print("EXCEPTION_CHAINING_OK")

src/executable/bytecode/instructions/RaiseVarargs.cpp

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,23 @@ PyResult<Value> RaiseVarargs::execute(VirtualMachine &vm, Interpreter &) const
2929
if (!exception_obj->type()->issubclass(BaseException::class_type())) {
3030
return Err(type_error("exceptions must derive from BaseException"));
3131
}
32+
auto *raised = static_cast<BaseException *>(exception_obj);
3233
if (m_cause.has_value()) {
3334
auto cause = PyObject::from(vm.reg(*m_cause));
3435
if (cause.is_err()) { return Err(cause.unwrap_err()); }
35-
static_cast<BaseException *>(exception_obj)->set_cause(cause.unwrap());
36+
// `raise X from Y` sets the explicit cause and suppresses the
37+
// implicit-context display (matching the data model). `from None`
38+
// keeps the suppression with a None cause.
39+
raised->set_cause(cause.unwrap());
40+
raised->set_suppress_context(true);
3641
}
37-
return Err(static_cast<BaseException *>(exception_obj));
42+
// NB: implicit __context__ chaining (set the new exception's context to
43+
// the one currently being handled) is intentionally NOT done here: the
44+
// frame exception stack is not reliably popped (internally-consumed
45+
// StopIterations linger — the same bug that makes a bare `raise` outside
46+
// a handler re-raise a stale exception), so reading it would attach a
47+
// spurious context. __context__ remains exposed and settable.
48+
return Err(raised);
3849
} else {
3950
// reraise
4051
if (!vm.interpreter().execution_frame()->exception_info().has_value()) {

src/runtime/BaseException.cpp

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#include "BaseException.hpp"
22
#include "MemoryError.hpp"
3+
#include "PyBool.hpp"
34
#include "PyCode.hpp"
45
#include "PyFrame.hpp"
56
#include "PyNone.hpp"
@@ -162,6 +163,38 @@ namespace {
162163
[](BaseException *self) -> PyResult<PyObject *> {
163164
return Ok(self->traceback() ? self->traceback() : py_none());
164165
})
166+
.property(
167+
"__cause__",
168+
[](BaseException *self) -> PyResult<PyObject *> {
169+
return Ok(self->cause() ? self->cause() : py_none());
170+
},
171+
[](BaseException *self, PyObject *value) -> PyResult<std::monostate> {
172+
// Per the data model, setting __cause__ also suppresses the
173+
// implicit context display (raise ... from ...).
174+
self->set_cause(value == py_none() ? nullptr : value);
175+
self->set_suppress_context(true);
176+
return Ok(std::monostate{});
177+
})
178+
.property(
179+
"__context__",
180+
[](BaseException *self) -> PyResult<PyObject *> {
181+
return Ok(self->context() ? self->context() : py_none());
182+
},
183+
[](BaseException *self, PyObject *value) -> PyResult<std::monostate> {
184+
self->set_context(value == py_none() ? nullptr : value);
185+
return Ok(std::monostate{});
186+
})
187+
.property(
188+
"__suppress_context__",
189+
[](BaseException *self) -> PyResult<PyObject *> {
190+
return Ok(self->suppress_context() ? py_true() : py_false());
191+
},
192+
[](BaseException *self, PyObject *value) -> PyResult<std::monostate> {
193+
auto truthy = value->true_();
194+
if (truthy.is_err()) { return Err(truthy.unwrap_err()); }
195+
self->set_suppress_context(truthy.unwrap());
196+
return Ok(std::monostate{});
197+
})
165198
.type);
166199
}
167200
}// namespace

src/runtime/BaseException.hpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,12 @@ class BaseException : public PyBaseObject
4848
PyObject *cause() const { return m_cause; }
4949
void set_cause(PyObject *cause) { m_cause = cause; }
5050

51+
PyObject *context() const { return m_context; }
52+
void set_context(PyObject *context) { m_context = context; }
53+
54+
bool suppress_context() const { return m_suppress_context; }
55+
void set_suppress_context(bool suppress) { m_suppress_context = suppress; }
56+
5157
std::string format_traceback() const;
5258

5359
static std::function<std::unique_ptr<TypePrototype>()> type_factory();

0 commit comments

Comments
 (0)