Skip to content

Commit cf68876

Browse files
authored
Fix locals()/vars() corrupting/leaking __conditional_annotations__ outside function scope (#8467)
CPython's _PyFrame_GetLocals only syncs fastlocals/cells into locals() for function scope; module/class scope just returns the namespace dict as-is. sync_visible_locals_to_mapping did this for every scope, so the implicit __conditional_annotations__ cell (used for PEP 649/749 deferred annotations) broke two ways: at module scope its cell is always empty, so syncing overwrote the dict's real value with None, causing NameError on the next annotated statement; at class scope its cell is the only real value, so syncing leaked it into locals()/dir(), unlike CPython. Skip cell/free slots outside function scope to match CPython, and add a regression snippet. Closes #8379 Assisted-by: Claude Code:claude-sonnet-5
1 parent cbaa589 commit cf68876

2 files changed

Lines changed: 54 additions & 3 deletions

File tree

crates/vm/src/frame.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1794,9 +1794,10 @@ impl FrameObject {
17941794
}
17951795
}
17961796

1797-
// Free variables only included for optimized (function-like) scopes.
1798-
// Class/module scopes should not expose free vars in locals().
1799-
if kind == CO_FAST_FREE && !is_optimized {
1797+
// CPython only syncs fastlocals/cells into locals() for function
1798+
// scope; class/module scope just returns the namespace dict as-is
1799+
// (_PyFrame_GetLocals, Objects/frameobject.c).
1800+
if !is_optimized && kind & (CO_FAST_CELL | CO_FAST_FREE) != 0 {
18001801
continue;
18011802
}
18021803

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
"""Module/class-scope locals() must not corrupt or leak __conditional_annotations__.
2+
3+
CPython's _PyFrame_GetLocals never syncs cell variables into a module/class
4+
scope's namespace dict, it just returns the dict directly (verified against
5+
CPython 3.14.6). __conditional_annotations__ is a cell in both scopes, but
6+
only module codegen also writes it into the dict (StoreName); class codegen
7+
only ever uses the cell (StoreDeref). So it's visible via locals()/dir() at
8+
module scope and absent at class scope.
9+
10+
RustPython's fast-locals-to-mapping sync used to read every cellvar's value
11+
straight from the cell regardless of scope. At module scope the cell is
12+
always empty, so this overwrote the dict's real value with None -- deleting
13+
it, and the next annotated statement raised NameError. At class scope it
14+
leaked __conditional_annotations__ into locals()/dir(), which CPython never
15+
does.
16+
"""
17+
18+
count: int = 1
19+
_ = locals()
20+
maybe: int = None # used to raise NameError before the fix
21+
assert maybe is None
22+
23+
assert "__conditional_annotations__" in dir(), (
24+
"module-level annotation should expose __conditional_annotations__, matching CPython"
25+
)
26+
27+
exec("a: int = 1\nlocals()\nb: int = 2")
28+
29+
if True:
30+
x: int = 1
31+
vars()
32+
if True:
33+
y: int = 2
34+
assert (x, y) == (1, 2)
35+
36+
37+
class C:
38+
if True:
39+
cx: int = 1
40+
locals()
41+
if True:
42+
cy: int = 2
43+
assert "__conditional_annotations__" not in dir(), (
44+
"class-level locals() should not leak __conditional_annotations__, matching CPython"
45+
)
46+
47+
48+
assert (C.cx, C.cy) == (1, 2)
49+
50+
print("ok")

0 commit comments

Comments
 (0)