From d6352b5b76d74024ea0065858fbdcc00bc765ed5 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Mon, 29 Sep 2025 12:12:29 +0100 Subject: [PATCH 1/2] [mypyc] Fix broken exception handling in generators Fix an unexpected undefined attribute error in a generator/async def. The intent was to explicitly check if an attribute was undefined, but the attribute read implicitly raised `AttributeError`, so the check was never reached. Fixed by allowing error values to be read. The error would looke like this (with no line number associated with it): ``` AttributeError("attribute '__mypyc_temp__12' of 'wait_Condition_gen' undefined") ``` --- mypyc/irbuild/builder.py | 17 +++++++++++++++-- mypyc/irbuild/statement.py | 9 ++++++++- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/mypyc/irbuild/builder.py b/mypyc/irbuild/builder.py index f4ee4371b9bf..fa0e605d6776 100644 --- a/mypyc/irbuild/builder.py +++ b/mypyc/irbuild/builder.py @@ -700,7 +700,12 @@ def get_assignment_target( assert False, "Unsupported lvalue: %r" % lvalue def read( - self, target: Value | AssignmentTarget, line: int = -1, can_borrow: bool = False + self, + target: Value | AssignmentTarget, + line: int = -1, + *, + can_borrow: bool = False, + allow_error_value: bool = False, ) -> Value: if isinstance(target, Value): return target @@ -716,7 +721,15 @@ def read( if isinstance(target, AssignmentTargetAttr): if isinstance(target.obj.type, RInstance) and target.obj.type.class_ir.is_ext_class: borrow = can_borrow and target.can_borrow - return self.add(GetAttr(target.obj, target.attr, line, borrow=borrow)) + return self.add( + GetAttr( + target.obj, + target.attr, + line, + borrow=borrow, + allow_error_value=allow_error_value, + ) + ) else: return self.py_get_attr(target.obj, target.attr, line) diff --git a/mypyc/irbuild/statement.py b/mypyc/irbuild/statement.py index c83c5550d059..fdcf4f777153 100644 --- a/mypyc/irbuild/statement.py +++ b/mypyc/irbuild/statement.py @@ -829,7 +829,14 @@ def transform_try_finally_stmt_async( # Check if we have a return value if ret_reg: return_block, check_old_exc = BasicBlock(), BasicBlock() - builder.add(Branch(builder.read(ret_reg), check_old_exc, return_block, Branch.IS_ERROR)) + builder.add( + Branch( + builder.read(ret_reg, allow_error_value=True), + check_old_exc, + return_block, + Branch.IS_ERROR, + ) + ) builder.activate_block(return_block) builder.nonlocal_control[-1].gen_return(builder, builder.read(ret_reg), -1) From 3325a8e9223dabe779f5bf5a9ef069def3444bc8 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Mon, 29 Sep 2025 13:55:11 +0100 Subject: [PATCH 2/2] Add test case --- mypyc/test-data/fixtures/testutil.py | 2 +- mypyc/test-data/run-async.test | 57 ++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/mypyc/test-data/fixtures/testutil.py b/mypyc/test-data/fixtures/testutil.py index 36ec41c8f38b..b2700f731ddd 100644 --- a/mypyc/test-data/fixtures/testutil.py +++ b/mypyc/test-data/fixtures/testutil.py @@ -44,7 +44,7 @@ def assertRaises(typ: type, msg: str = '') -> Iterator[None]: try: yield - except Exception as e: + except BaseException as e: assert type(e) is typ, f"{e!r} is not a {typ.__name__}" assert msg in str(e), f'Message "{e}" does not match "{msg}"' else: diff --git a/mypyc/test-data/run-async.test b/mypyc/test-data/run-async.test index 55cde4ab44f1..94a1cd2e97c5 100644 --- a/mypyc/test-data/run-async.test +++ b/mypyc/test-data/run-async.test @@ -1234,3 +1234,60 @@ def test_callable_arg_same_name_as_helper() -> None: [file asyncio/__init__.pyi] def run(x: object) -> object: ... + +[case testRunAsyncCancelFinallySpecialCase] +import asyncio + +from testutil import assertRaises + +# Greatly simplified from asyncio.Condition +class Condition: + async def acquire(self) -> None: pass + + async def wait(self) -> bool: + l = asyncio.get_running_loop() + fut = l.create_future() + a = [] + try: + try: + a.append(fut) + try: + await fut + return True + finally: + a.pop() + finally: + err = None + while True: + try: + await self.acquire() + break + except asyncio.CancelledError as e: + err = e + + if err is not None: + try: + raise err + finally: + err = None + except BaseException: + raise + +async def do_cancel() -> None: + cond = Condition() + wait = asyncio.create_task(cond.wait()) + asyncio.get_running_loop().call_soon(wait.cancel) + with assertRaises(asyncio.CancelledError): + await wait + +def test_cancel_special_case() -> None: + asyncio.run(do_cancel()) + +[file asyncio/__init__.pyi] +from typing import Any + +class CancelledError(Exception): ... + +def run(x: object) -> object: ... +def get_running_loop() -> Any: ... +def create_task(x: object) -> Any: ...