Skip to content

Commit df4ae6a

Browse files
authored
[mypyc] Fix broken exception/cancellation handling in async def (#19951)
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 without raising `AttributeError`. The error this fixes would look like this (with no line number associated with it): ``` AttributeError("attribute '__mypyc_temp__12' of 'wait_Condition_gen' undefined") ```
1 parent 16cd4c5 commit df4ae6a

File tree

4 files changed

+81
-4
lines changed

4 files changed

+81
-4
lines changed

mypyc/irbuild/builder.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -700,7 +700,12 @@ def get_assignment_target(
700700
assert False, "Unsupported lvalue: %r" % lvalue
701701

702702
def read(
703-
self, target: Value | AssignmentTarget, line: int = -1, can_borrow: bool = False
703+
self,
704+
target: Value | AssignmentTarget,
705+
line: int = -1,
706+
*,
707+
can_borrow: bool = False,
708+
allow_error_value: bool = False,
704709
) -> Value:
705710
if isinstance(target, Value):
706711
return target
@@ -716,7 +721,15 @@ def read(
716721
if isinstance(target, AssignmentTargetAttr):
717722
if isinstance(target.obj.type, RInstance) and target.obj.type.class_ir.is_ext_class:
718723
borrow = can_borrow and target.can_borrow
719-
return self.add(GetAttr(target.obj, target.attr, line, borrow=borrow))
724+
return self.add(
725+
GetAttr(
726+
target.obj,
727+
target.attr,
728+
line,
729+
borrow=borrow,
730+
allow_error_value=allow_error_value,
731+
)
732+
)
720733
else:
721734
return self.py_get_attr(target.obj, target.attr, line)
722735

mypyc/irbuild/statement.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -829,7 +829,14 @@ def transform_try_finally_stmt_async(
829829
# Check if we have a return value
830830
if ret_reg:
831831
return_block, check_old_exc = BasicBlock(), BasicBlock()
832-
builder.add(Branch(builder.read(ret_reg), check_old_exc, return_block, Branch.IS_ERROR))
832+
builder.add(
833+
Branch(
834+
builder.read(ret_reg, allow_error_value=True),
835+
check_old_exc,
836+
return_block,
837+
Branch.IS_ERROR,
838+
)
839+
)
833840

834841
builder.activate_block(return_block)
835842
builder.nonlocal_control[-1].gen_return(builder, builder.read(ret_reg), -1)

mypyc/test-data/fixtures/testutil.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
def assertRaises(typ: type, msg: str = '') -> Iterator[None]:
4545
try:
4646
yield
47-
except Exception as e:
47+
except BaseException as e:
4848
assert type(e) is typ, f"{e!r} is not a {typ.__name__}"
4949
assert msg in str(e), f'Message "{e}" does not match "{msg}"'
5050
else:

mypyc/test-data/run-async.test

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1234,3 +1234,60 @@ def test_callable_arg_same_name_as_helper() -> None:
12341234

12351235
[file asyncio/__init__.pyi]
12361236
def run(x: object) -> object: ...
1237+
1238+
[case testRunAsyncCancelFinallySpecialCase]
1239+
import asyncio
1240+
1241+
from testutil import assertRaises
1242+
1243+
# Greatly simplified from asyncio.Condition
1244+
class Condition:
1245+
async def acquire(self) -> None: pass
1246+
1247+
async def wait(self) -> bool:
1248+
l = asyncio.get_running_loop()
1249+
fut = l.create_future()
1250+
a = []
1251+
try:
1252+
try:
1253+
a.append(fut)
1254+
try:
1255+
await fut
1256+
return True
1257+
finally:
1258+
a.pop()
1259+
finally:
1260+
err = None
1261+
while True:
1262+
try:
1263+
await self.acquire()
1264+
break
1265+
except asyncio.CancelledError as e:
1266+
err = e
1267+
1268+
if err is not None:
1269+
try:
1270+
raise err
1271+
finally:
1272+
err = None
1273+
except BaseException:
1274+
raise
1275+
1276+
async def do_cancel() -> None:
1277+
cond = Condition()
1278+
wait = asyncio.create_task(cond.wait())
1279+
asyncio.get_running_loop().call_soon(wait.cancel)
1280+
with assertRaises(asyncio.CancelledError):
1281+
await wait
1282+
1283+
def test_cancel_special_case() -> None:
1284+
asyncio.run(do_cancel())
1285+
1286+
[file asyncio/__init__.pyi]
1287+
from typing import Any
1288+
1289+
class CancelledError(Exception): ...
1290+
1291+
def run(x: object) -> object: ...
1292+
def get_running_loop() -> Any: ...
1293+
def create_task(x: object) -> Any: ...

0 commit comments

Comments
 (0)