Compare commits

...

9 Commits

Author SHA1 Message Date
Kanika Kapoor
985fd2aac7
Merge c048ec9faf4bfe02da004dce69471897933c3617 into 005a2725a1bb51e2af3591d41cbe3f46f6bde60c 2026-03-24 18:26:55 +02:00
blueloveTH
005a2725a1 add PK_ENABLE_DLL 2026-03-24 11:02:34 +08:00
Daniel Calderón
d03b067666
adding missing header guard (#483) 2026-03-23 11:58:33 +08:00
Jason Matthew Suhari
0676b21da2
fix: save stack checkpoint before pushing args in operator() (#479)
* fix: capture stack checkpoint before pushing args in operator() (#469)

* test: add regression test for operator() python error propagation (#469)
2026-03-20 13:22:25 +08:00
blueloveTH
a2f16e5f1f pre-release 2026-03-18 15:10:24 +08:00
blueloveTH
a69cca59f4 post fix
Update dll.c

Update dll.c
2026-03-18 15:04:26 +08:00
kushagra-1809
7614bdcc4a
Wrong formulas (#470)
The correct formulas for complex trigonometry require
cos(z) = (exp(iz) + exp(-iz)) / 2
sin(z) = (exp(iz) - exp(-iz)) / (2i)
2026-03-18 14:58:23 +08:00
wdskuki
984c0eefcc
Fix C extension module import on Linux (#472)
* Fix C extension module import on Linux

* minor fix

---------

Co-authored-by: wdsmini <wdsmini@wdsmini.local>
Co-authored-by: blueloveTH <blueloveTH@foxmail.com>
2026-03-18 14:55:38 +08:00
Kanika Kapoor
c048ec9faf Fix context manager __exit__ not being called on exception (#395)
Problem: When an exception occurs in a WITH block, __exit__ was not called,
preventing proper cleanup of context managers.

Solution:
1. Wrap WITH block body in try-except structure
2. On normal exit: call __exit__(None, None, None)
3. On exception: call __exit__ with exception info before re-raising

Changes:
- compiler.c: Wrap WITH body in try-except, ensure __exit__ called in both paths
- ceval.c: Update OP_WITH_EXIT to accept three arguments (exc_type, exc_val, exc_tb)
- tests/520_context.py: Add test to verify __exit__ called on exceptions
2025-12-27 01:12:15 +05:30
13 changed files with 239 additions and 45 deletions

View File

@ -61,6 +61,12 @@ else()
add_definitions(-DPK_ENABLE_THREADS=0) add_definitions(-DPK_ENABLE_THREADS=0)
endif() endif()
if(PK_ENABLE_DLL)
add_definitions(-DPK_ENABLE_DLL=1)
else()
add_definitions(-DPK_ENABLE_DLL=0)
endif()
if(PK_ENABLE_DETERMINISM) if(PK_ENABLE_DETERMINISM)
add_definitions(-DPK_ENABLE_DETERMINISM=1) add_definitions(-DPK_ENABLE_DETERMINISM=1)
else() else()
@ -154,7 +160,7 @@ if(PK_ENABLE_THREADS)
endif() endif()
if(UNIX AND NOT APPLE) if(UNIX AND NOT APPLE)
if(PK_ENABLE_OS) if(PK_ENABLE_OS AND PK_ENABLE_DLL)
target_link_libraries(${PROJECT_NAME} dl) target_link_libraries(${PROJECT_NAME} dl)
endif() endif()
elseif(WIN32) elseif(WIN32)

View File

@ -8,6 +8,7 @@ endif()
# system features # system features
option(PK_ENABLE_OS "" ON) option(PK_ENABLE_OS "" ON)
option(PK_ENABLE_THREADS "" ON) option(PK_ENABLE_THREADS "" ON)
option(PK_ENABLE_DLL "" ON)
option(PK_ENABLE_DETERMINISM "" ON) option(PK_ENABLE_DETERMINISM "" ON)
option(PK_ENABLE_WATCHDOG "" OFF) option(PK_ENABLE_WATCHDOG "" OFF)
option(PK_ENABLE_CUSTOM_SNAME "" OFF) option(PK_ENABLE_CUSTOM_SNAME "" OFF)

View File

@ -1,18 +1,22 @@
#pragma once #pragma once
// clang-format off // clang-format off
#define PK_VERSION "2.1.8" #define PK_VERSION "2.1.9"
#define PK_VERSION_MAJOR 2 #define PK_VERSION_MAJOR 2
#define PK_VERSION_MINOR 1 #define PK_VERSION_MINOR 1
#define PK_VERSION_PATCH 8 #define PK_VERSION_PATCH 9
/*************** feature settings ***************/ /*************** feature settings ***************/
#ifndef PK_ENABLE_OS // can be overridden by cmake #ifndef PK_ENABLE_OS // can be overridden by cmake
#define PK_ENABLE_OS 1 #define PK_ENABLE_OS 1
#endif #endif
#ifndef PK_ENABLE_THREADS // can be overridden by cmake #ifndef PK_ENABLE_THREADS // must be enabled from cmake
#define PK_ENABLE_THREADS 1 #define PK_ENABLE_THREADS 0
#endif
#ifndef PK_ENABLE_DLL // must be enabled from cmake
#define PK_ENABLE_DLL 0
#endif #endif
#ifndef PK_ENABLE_DETERMINISM // must be enabled from cmake #ifndef PK_ENABLE_DETERMINISM // must be enabled from cmake

View File

@ -70,6 +70,7 @@ args_proxy interface<Derived>::operator* () const {
template <typename Derived> template <typename Derived>
template <return_value_policy policy, typename... Args> template <return_value_policy policy, typename... Args>
object interface<Derived>::operator() (Args&&... args) const { object interface<Derived>::operator() (Args&&... args) const {
py_StackRef p0 = py_peek(0); // checkpoint before pushing so py_clearexc can safely rewind
py_push(ptr()); py_push(ptr());
py_pushnil(); py_pushnil();
@ -108,7 +109,13 @@ object interface<Derived>::operator() (Args&&... args) const {
(foreach(std::forward<Args>(args)), ...); (foreach(std::forward<Args>(args)), ...);
raise_call<py_vectorcall>(argc, kwargsc); if(!py_vectorcall(argc, kwargsc)) {
py_matchexc(tp_Exception);
object e = object::from_ret();
auto what = py_formatexc();
py_clearexc(p0);
throw python_error(what, std::move(e));
}
return object::from_ret(); return object::from_ret();
} }

View File

@ -1,3 +1,5 @@
#pragma once
#include "pybind11.h" #include "pybind11.h"
#include <array> #include <array>

View File

@ -79,3 +79,18 @@ TEST_F(PYBIND11_TEST, exception_cpp_to_python) {
TEST_EXCEPTION(attribute_error, AttributeError); TEST_EXCEPTION(attribute_error, AttributeError);
TEST_EXCEPTION(runtime_error, RuntimeError); TEST_EXCEPTION(runtime_error, RuntimeError);
} }
// Regression test: operator() must throw python_error instead of crashing when Python raises (#469)
TEST_F(PYBIND11_TEST, operator_call_propagates_python_error) {
py::exec("def f(x):\n raise ValueError('intentional error')");
py::object fn = py::eval("f");
bool caught = false;
try {
fn(py::int_(1));
} catch(py::python_error& e) {
caught = true;
EXPECT_TRUE(e.match(tp_ValueError));
}
EXPECT_TRUE(caught);
}

View File

@ -1530,9 +1530,9 @@ class PocketpyBindings {
ffi.NativeFunction< ffi.NativeFunction<
ffi.Bool Function(py_Ref self, py_Name name)>>)>(); ffi.Bool Function(py_Ref self, py_Name name)>>)>();
/// Get the current `function` object on the stack. /// Get the current `Callable` object on the stack of the most recent vectorcall.
/// Return `NULL` if not available. /// Return `NULL` if not available.
/// NOTE: This function should be placed at the beginning of your decl-based bindings. /// NOTE: This function should be placed at the beginning of your bindings or you will get wrong result.
py_StackRef py_inspect_currentfunction() { py_StackRef py_inspect_currentfunction() {
return _py_inspect_currentfunction(); return _py_inspect_currentfunction();
} }
@ -3635,6 +3635,22 @@ class PocketpyBindings {
late final _py_newvec3i = late final _py_newvec3i =
_py_newvec3iPtr.asFunction<void Function(py_OutRef, c11_vec3i)>(); _py_newvec3iPtr.asFunction<void Function(py_OutRef, c11_vec3i)>();
void py_newvec4i(
py_OutRef out,
c11_vec4i arg1,
) {
return _py_newvec4i(
out,
arg1,
);
}
late final _py_newvec4iPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(py_OutRef, c11_vec4i)>>(
'py_newvec4i');
late final _py_newvec4i =
_py_newvec4iPtr.asFunction<void Function(py_OutRef, c11_vec4i)>();
void py_newcolor32( void py_newcolor32(
py_OutRef out, py_OutRef out,
c11_color32 arg1, c11_color32 arg1,
@ -3715,6 +3731,19 @@ class PocketpyBindings {
late final _py_tovec3i = late final _py_tovec3i =
_py_tovec3iPtr.asFunction<c11_vec3i Function(py_Ref)>(); _py_tovec3iPtr.asFunction<c11_vec3i Function(py_Ref)>();
c11_vec4i py_tovec4i(
py_Ref self,
) {
return _py_tovec4i(
self,
);
}
late final _py_tovec4iPtr =
_lookup<ffi.NativeFunction<c11_vec4i Function(py_Ref)>>('py_tovec4i');
late final _py_tovec4i =
_py_tovec4iPtr.asFunction<c11_vec4i Function(py_Ref)>();
ffi.Pointer<c11_mat3x3> py_tomat3x3( ffi.Pointer<c11_mat3x3> py_tomat3x3(
py_Ref self, py_Ref self,
) { ) {
@ -3932,10 +3961,32 @@ final class UnnamedUnion1 extends ffi.Union {
@ffi.Int64() @ffi.Int64()
external int _i64; external int _i64;
@ffi.Double()
external double _f64;
@ffi.Bool()
external bool _bool;
external py_CFunction _cfunc;
external ffi.Pointer<ffi.Void> _obj;
external ffi.Pointer<ffi.Void> _ptr;
@ffi.Array.multi([16]) @ffi.Array.multi([16])
external ffi.Array<ffi.Char> _chars; external ffi.Array<ffi.Char> _chars;
} }
/// Native function signature.
/// @param argc number of arguments.
/// @param argv array of arguments. Use `py_arg(i)` macro to get the i-th argument.
/// @return `true` if the function is successful or `false` if an exception is raised.
typedef py_CFunction = ffi.Pointer<
ffi.NativeFunction<ffi.Bool Function(ffi.Int argc, py_StackRef argv)>>;
/// A specific location in the value stack of the VM.
typedef py_StackRef = ffi.Pointer<py_TValue>;
/// A string view type. It is helpful for passing strings which are not null-terminated. /// A string view type. It is helpful for passing strings which are not null-terminated.
final class c11_sv extends ffi.Struct { final class c11_sv extends ffi.Struct {
external ffi.Pointer<ffi.Char> data; external ffi.Pointer<ffi.Char> data;
@ -4027,22 +4078,12 @@ typedef py_TraceFunc = ffi.Pointer<
/// An output reference for returning a value. Only use this for function arguments. /// An output reference for returning a value. Only use this for function arguments.
typedef py_OutRef = ffi.Pointer<py_TValue>; typedef py_OutRef = ffi.Pointer<py_TValue>;
/// A specific location in the value stack of the VM.
typedef py_StackRef = ffi.Pointer<py_TValue>;
/// A 64-bit integer type. Corresponds to `int` in python. /// A 64-bit integer type. Corresponds to `int` in python.
typedef py_i64 = ffi.Int64; typedef py_i64 = ffi.Int64;
/// A 64-bit floating-point type. Corresponds to `float` in python. /// A 64-bit floating-point type. Corresponds to `float` in python.
typedef py_f64 = ffi.Double; typedef py_f64 = ffi.Double;
/// Native function signature.
/// @param argc number of arguments.
/// @param argv array of arguments. Use `py_arg(i)` macro to get the i-th argument.
/// @return `true` if the function is successful or `false` if an exception is raised.
typedef py_CFunction = ffi.Pointer<
ffi.NativeFunction<ffi.Bool Function(ffi.Int argc, py_StackRef argv)>>;
/// A pointer that represents a python identifier. For fast name resolution. /// A pointer that represents a python identifier. For fast name resolution.
typedef py_Name = ffi.Pointer<py_OpaqueName>; typedef py_Name = ffi.Pointer<py_OpaqueName>;
@ -4125,9 +4166,30 @@ final class UnnamedStruct4 extends ffi.Struct {
external int z; external int z;
} }
final class c11_color32 extends ffi.Union { final class c11_vec4i extends ffi.Union {
external UnnamedStruct5 unnamed; external UnnamedStruct5 unnamed;
@ffi.Array.multi([4])
external ffi.Array<ffi.Int> data;
}
final class UnnamedStruct5 extends ffi.Struct {
@ffi.Int()
external int x;
@ffi.Int()
external int y;
@ffi.Int()
external int z;
@ffi.Int()
external int w;
}
final class c11_color32 extends ffi.Union {
external UnnamedStruct6 unnamed;
@ffi.Array.multi([4]) @ffi.Array.multi([4])
external ffi.Array<ffi.UnsignedChar> data; external ffi.Array<ffi.UnsignedChar> data;
@ -4135,7 +4197,7 @@ final class c11_color32 extends ffi.Union {
external int u32; external int u32;
} }
final class UnnamedStruct5 extends ffi.Struct { final class UnnamedStruct6 extends ffi.Struct {
@ffi.UnsignedChar() @ffi.UnsignedChar()
external int r; external int r;
@ -4150,7 +4212,7 @@ final class UnnamedStruct5 extends ffi.Struct {
} }
final class c11_mat3x3 extends ffi.Union { final class c11_mat3x3 extends ffi.Union {
external UnnamedStruct6 unnamed; external UnnamedStruct7 unnamed;
@ffi.Array.multi([3, 3]) @ffi.Array.multi([3, 3])
external ffi.Array<ffi.Array<ffi.Float>> m; external ffi.Array<ffi.Array<ffi.Float>> m;
@ -4159,7 +4221,7 @@ final class c11_mat3x3 extends ffi.Union {
external ffi.Array<ffi.Float> data; external ffi.Array<ffi.Float> data;
} }
final class UnnamedStruct6 extends ffi.Struct { final class UnnamedStruct7 extends ffi.Struct {
@ffi.Float() @ffi.Float()
external double _11; external double _11;
@ -4306,13 +4368,14 @@ abstract class py_PredefinedType {
static const int tp_vec3 = 71; static const int tp_vec3 = 71;
static const int tp_vec2i = 72; static const int tp_vec2i = 72;
static const int tp_vec3i = 73; static const int tp_vec3i = 73;
static const int tp_mat3x3 = 74; static const int tp_vec4i = 74;
static const int tp_color32 = 75; static const int tp_mat3x3 = 75;
static const int tp_color32 = 76;
/// array2d /// array2d
static const int tp_array2d_like = 76; static const int tp_array2d_like = 77;
static const int tp_array2d_like_iterator = 77; static const int tp_array2d_like_iterator = 78;
static const int tp_array2d = 78; static const int tp_array2d = 79;
static const int tp_array2d_view = 79; static const int tp_array2d_view = 80;
static const int tp_chunked_array2d = 80; static const int tp_chunked_array2d = 81;
} }

View File

@ -134,10 +134,10 @@ def atan(z: complex):
return 1j / 2 * log((1 - 1j * z) / (1 + 1j * z)) return 1j / 2 * log((1 - 1j * z) / (1 + 1j * z))
def cos(z: complex): def cos(z: complex):
return (exp(z) + exp(-z)) / 2 return (exp(1j * z) + exp(-1j * z)) / 2
def sin(z: complex): def sin(z: complex):
return (exp(z) - exp(-z)) / (2 * 1j) return (exp(1j * z) - exp(-1j * z)) / (2 * 1j)
def tan(z: complex): def tan(z: complex):
return sin(z) / cos(z) return sin(z) / cos(z)

File diff suppressed because one or more lines are too long

View File

@ -2801,6 +2801,8 @@ static Error* compile_stmt(Compiler* self) {
case TK_WITH: { case TK_WITH: {
check(EXPR(self)); // [ <expr> ] check(EXPR(self)); // [ <expr> ]
Ctx__s_emit_top(ctx()); Ctx__s_emit_top(ctx());
// Save context manager for later __exit__ call
Ctx__emit_(ctx(), OP_DUP_TOP, BC_NOARG, prev()->line);
Ctx__enter_block(ctx(), CodeBlockType_WITH); Ctx__enter_block(ctx(), CodeBlockType_WITH);
NameExpr* as_name = NULL; NameExpr* as_name = NULL;
if(match(TK_AS)) { if(match(TK_AS)) {
@ -2809,17 +2811,33 @@ static Error* compile_stmt(Compiler* self) {
as_name = NameExpr__new(prev()->line, name, name_scope(self)); as_name = NameExpr__new(prev()->line, name, name_scope(self));
} }
Ctx__emit_(ctx(), OP_WITH_ENTER, BC_NOARG, prev()->line); Ctx__emit_(ctx(), OP_WITH_ENTER, BC_NOARG, prev()->line);
// [ <expr> <expr>.__enter__() ]
if(as_name) { if(as_name) {
bool ok = vtemit_store((Expr*)as_name, ctx()); bool ok = vtemit_store((Expr*)as_name, ctx());
vtdelete((Expr*)as_name); vtdelete((Expr*)as_name);
if(!ok) return SyntaxError(self, "invalid syntax"); if(!ok) return SyntaxError(self, "invalid syntax");
} else { } else {
// discard `__enter__()`'s return value
Ctx__emit_(ctx(), OP_POP_TOP, BC_NOARG, BC_KEEPLINE); Ctx__emit_(ctx(), OP_POP_TOP, BC_NOARG, BC_KEEPLINE);
} }
// Wrap body in try-except to ensure __exit__ is called even on exception
Ctx__enter_block(ctx(), CodeBlockType_TRY);
Ctx__emit_(ctx(), OP_BEGIN_TRY, BC_NOARG, prev()->line);
check(compile_block_body(self)); check(compile_block_body(self));
Ctx__emit_(ctx(), OP_END_TRY, BC_NOARG, BC_KEEPLINE);
// Normal exit: call __exit__(None, None, None)
Ctx__emit_(ctx(), OP_LOAD_NONE, BC_NOARG, prev()->line);
Ctx__emit_(ctx(), OP_LOAD_NONE, BC_NOARG, prev()->line);
Ctx__emit_(ctx(), OP_LOAD_NONE, BC_NOARG, prev()->line);
Ctx__emit_(ctx(), OP_WITH_EXIT, BC_NOARG, prev()->line); Ctx__emit_(ctx(), OP_WITH_EXIT, BC_NOARG, prev()->line);
int jump_patch = Ctx__emit_(ctx(), OP_JUMP_FORWARD, BC_NOARG, BC_KEEPLINE);
Ctx__exit_block(ctx());
// Exception handler: call __exit__ with exception info, then re-raise
Ctx__emit_(ctx(), OP_PUSH_EXCEPTION, BC_NOARG, BC_KEEPLINE);
Ctx__emit_(ctx(), OP_LOAD_NONE, BC_NOARG, BC_KEEPLINE); // exc_type
Ctx__emit_(ctx(), OP_ROT_TWO, BC_NOARG, BC_KEEPLINE); // reorder: [cm, None, exc]
Ctx__emit_(ctx(), OP_LOAD_NONE, BC_NOARG, BC_KEEPLINE); // exc_tb
Ctx__emit_(ctx(), OP_WITH_EXIT, BC_NOARG, prev()->line);
Ctx__emit_(ctx(), OP_RE_RAISE, BC_NOARG, BC_KEEPLINE);
Ctx__patch_jump(ctx(), jump_patch);
Ctx__exit_block(ctx()); Ctx__exit_block(ctx());
} break; } break;
/*************************************************/ /*************************************************/

View File

@ -1122,14 +1122,35 @@ __NEXT_STEP:
DISPATCH(); DISPATCH();
} }
case OP_WITH_EXIT: { case OP_WITH_EXIT: {
// [expr] // Stack: [cm, exc_type, exc_val, exc_tb]
py_push(TOP()); // Call cm.__exit__(exc_type, exc_val, exc_tb)
py_Ref exc_tb = TOP();
py_Ref exc_val = SECOND();
py_Ref exc_type = THIRD();
py_Ref cm = FOURTH();
// Save all values from stack
py_TValue saved_cm = *cm;
py_TValue saved_exc_type = *exc_type;
py_TValue saved_exc_val = *exc_val;
py_TValue saved_exc_tb = *exc_tb;
self->stack.sp -= 4;
// Push cm and get __exit__ method
py_push(&saved_cm);
if(!py_pushmethod(__exit__)) { if(!py_pushmethod(__exit__)) {
TypeError("'%t' object does not support the context manager protocol", TOP()->type); TypeError("'%t' object does not support the context manager protocol", saved_cm.type);
goto __ERROR; goto __ERROR;
} }
if(!py_vectorcall(0, 0)) goto __ERROR;
POP(); // Push arguments: exc_type, exc_val, exc_tb
PUSH(&saved_exc_type);
PUSH(&saved_exc_val);
PUSH(&saved_exc_tb);
// Call __exit__(exc_type, exc_val, exc_tb)
if(!py_vectorcall(3, 0)) goto __ERROR;
py_pop(); // discard return value
DISPATCH(); DISPATCH();
} }
/////////// ///////////

View File

@ -1,6 +1,6 @@
#include "pocketpy/pocketpy.h" #include "pocketpy/pocketpy.h"
#if PK_IS_DESKTOP_PLATFORM && PK_ENABLE_OS #if PK_IS_DESKTOP_PLATFORM && PK_ENABLE_OS && PK_ENABLE_DLL
#ifdef _WIN32 #ifdef _WIN32
@ -10,6 +10,8 @@
#else #else
#include <dlfcn.h> #include <dlfcn.h>
#include <stdlib.h>
#include <string.h>
#endif #endif
typedef bool (*py_module_initialize_t)() PY_RAISE PY_RETURN; typedef bool (*py_module_initialize_t)() PY_RAISE PY_RETURN;
@ -21,14 +23,44 @@ int load_module_from_dll_desktop_only(const char* path) PY_RAISE PY_RETURN {
if(dll == NULL) return 0; if(dll == NULL) return 0;
py_module_initialize_t f_init = (py_module_initialize_t)GetProcAddress(dll, f_init_name); py_module_initialize_t f_init = (py_module_initialize_t)GetProcAddress(dll, f_init_name);
#else #else
void* dll = dlopen(path, RTLD_LAZY); void* dll = NULL;
// On Linux, dlopen doesn't automatically add .so suffix like Windows does with .dll
// Also, CMake typically generates libXxx.so instead of Xxx.so
// Try: path.so, libpath.so, then the original path
char* path_with_so = NULL;
char* path_with_lib = NULL;
size_t path_len = strlen(path);
// Try path.so
path_with_so = py_malloc(path_len + 4); // .so + null terminator
if(path_with_so != NULL) {
strcpy(path_with_so, path);
strcat(path_with_so, ".so");
dll = dlopen(path_with_so, RTLD_LAZY);
py_free(path_with_so);
}
// Try libpath.so if path.so didn't work
if(dll == NULL) {
path_with_lib = py_malloc(path_len + 7); // lib + .so + null terminator
if(path_with_lib != NULL) {
strcpy(path_with_lib, "lib");
strcat(path_with_lib, path);
strcat(path_with_lib, ".so");
dll = dlopen(path_with_lib, RTLD_LAZY);
py_free(path_with_lib);
}
}
// Fallback to original path
if(dll == NULL) {
dll = dlopen(path, RTLD_LAZY);
}
if(dll == NULL) return 0; if(dll == NULL) return 0;
py_module_initialize_t f_init = (py_module_initialize_t)dlsym(dll, f_init_name); py_module_initialize_t f_init = (py_module_initialize_t)dlsym(dll, f_init_name);
#endif #endif
if(f_init == NULL) { if(f_init == NULL) return 0;
RuntimeError("%s() not found in '%s'", f_init_name, path);
return -1;
}
bool success = f_init(); bool success = f_init();
if(!success) return -1; if(!success) return -1;
return 1; return 1;

View File

@ -27,4 +27,29 @@ assert path == ['enter', 'in', 'exit']
path.clear() path.clear()
# Test that __exit__ is called even when an exception occurs
class B:
def __init__(self):
self.path = []
def __enter__(self):
path.append('enter')
return self
def __exit__(self, exc_type, exc_val, exc_tb):
path.append('exit')
if exc_type is not None:
path.append('exception')
return False # propagate exception
try:
with B():
path.append('before_raise')
raise ValueError('test')
path.append('after_raise') # should not be reached
except ValueError:
pass
assert path == ['enter', 'before_raise', 'exit', 'exception'], f"Expected ['enter', 'before_raise', 'exit', 'exception'], got {path}"