mirror of
https://github.com/RustPython/RustPython.git
synced 2026-10-28 18:59:04 +08:00
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
This commit is contained in:
1 parent
25e2df1731
commit
7545bb1993
2 files changed
+54
-3
No files matched your search
@@ -1794,9 +1794,10 @@ impl FrameObject {
|
||||
}
|
||||
}
|
||||
|
||||
// Free variables only included for optimized (function-like) scopes.
|
||||
// Class/module scopes should not expose free vars in locals().
|
||||
if kind == CO_FAST_FREE && !is_optimized {
|
||||
// CPython only syncs fastlocals/cells into locals() for function
|
||||
// scope; class/module scope just returns the namespace dict as-is
|
||||
// (_PyFrame_GetLocals, Objects/frameobject.c).
|
||||
if !is_optimized && kind & (CO_FAST_CELL | CO_FAST_FREE) != 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
"""Module/class-scope locals() must not corrupt or leak __conditional_annotations__.
|
||||
|
||||
CPython's _PyFrame_GetLocals never syncs cell variables into a module/class
|
||||
scope's namespace dict, it just returns the dict directly (verified against
|
||||
CPython 3.14.6). __conditional_annotations__ is a cell in both scopes, but
|
||||
only module codegen also writes it into the dict (StoreName); class codegen
|
||||
only ever uses the cell (StoreDeref). So it's visible via locals()/dir() at
|
||||
module scope and absent at class scope.
|
||||
|
||||
RustPython's fast-locals-to-mapping sync used to read every cellvar's value
|
||||
straight from the cell regardless of scope. At module scope the cell is
|
||||
always empty, so this overwrote the dict's real value with None -- deleting
|
||||
it, and the next annotated statement raised NameError. At class scope it
|
||||
leaked __conditional_annotations__ into locals()/dir(), which CPython never
|
||||
does.
|
||||
"""
|
||||
|
||||
count: int = 1
|
||||
_ = locals()
|
||||
maybe: int = None # used to raise NameError before the fix
|
||||
assert maybe is None
|
||||
|
||||
assert "__conditional_annotations__" in dir(), (
|
||||
"module-level annotation should expose __conditional_annotations__, matching CPython"
|
||||
)
|
||||
|
||||
exec("a: int = 1\nlocals()\nb: int = 2")
|
||||
|
||||
if True:
|
||||
x: int = 1
|
||||
vars()
|
||||
if True:
|
||||
y: int = 2
|
||||
assert (x, y) == (1, 2)
|
||||
|
||||
|
||||
class C:
|
||||
if True:
|
||||
cx: int = 1
|
||||
locals()
|
||||
if True:
|
||||
cy: int = 2
|
||||
assert "__conditional_annotations__" not in dir(), (
|
||||
"class-level locals() should not leak __conditional_annotations__, matching CPython"
|
||||
)
|
||||
|
||||
|
||||
assert (C.cx, C.cy) == (1, 2)
|
||||
|
||||
print("ok")
|
||||
Reference in new issue
Block a user