Implement PEP 814 frozendict (#8951)

* Restore consolidated frozendict implementation on current main

Preserve reverse dictionary iterator fixes and the 3.15 frame imports. Keep library refreshes separate pending the runtime dependency sequencing decision.

Assisted-by: OpenAI Codex:model identifier unavailable

* Support scoped canonical test updates in update_lib

Index conditional test classes and methods without crossing namespaces, reject ambiguous targets, and preserve inherited override indentation. Add hash-pinned whole-test copying that verifies execution context and exact source bytes, with focused Python fixtures.

Assisted-by: OpenAI Codex:model identifier unavailable

* Align frozendict copy support and tests with CPython 3.15

Import exact-final copy.py with update_lib; defer Mapping ABC registration to the later library stage. Generate only proven marker corrections, retain observed debug recursion-limit expected failures, and copy the complete canonical builtin-doc discovery test using hash-pinned update_lib copy-test.

Assisted-by: OpenAI Codex:model identifier unavailable

* Preserve decorator rationale during canonical test copying

Reject differing decorator source blocks even when their ASTs match. Cover inline and multiline marker comments, guarded async methods, and CLI no-write behavior.

Assisted-by: OpenAI Codex:model identifier unavailable

* Finish frozendict with borrowed PyAnyDict views

Use the landed PyRef target model for a transparent dict/frozendict header
view, retaining objects explicitly with PyRef<PyAnyDict>. Keep targets
sealed and preserve mutable-only entry points. Borrow frame globals from
the object header rather than movable reference wrappers.

Unify exact-TypeError key diagnostics with final CPython semantics,
including callback failure propagation and no explicit cause. Keep the
Python regressions in builtin_dict and test reference layout/ownership
in Rust. Preserve existing canonical library updates.

Assisted-by: OpenAI Codex:model identifier unavailable

* Align frozendict with CPython and use CPython 3.15 tests

- Name the target type in unhashable key errors through
  PyDict::update_arg (dict_update_arg), replacing the separate
  PyFrozenDict::merge_into copy of the merge logic.
- Split the | operator into dict_or (_PyDict_Or) and frozendict_or.
- Return the argument from frozendict(frozendict) only when the called
  type is frozendict.
- Call through the vectorcall slot in PyCallable::invoke when there
  are no keyword arguments, instead of special-casing frozendict.
- Add ensure_builtins (_PyEval_EnsureBuiltins) for eval/exec, and
  insert __builtins__ for source strings regardless of globals type.
- Read __warningregistry__ and __name__ from stored globals entries and
  accept only None or str for the module name in setup_context.
- Remove the unused PyFrozenDict::from_object.
- Migrate test_dict.py, mapping_tests.py, test_copy.py and
  test_builtin.py to v3.15.0 with update_lib, and add
  test_import/data/syntax_warnings.py.
- Drop builtin_dict.py frozendict tests covered by those modules,
  keeping the descriptor and namespace integration tests.

Assisted-by: Claude Code:claude-opus-5-5

* Update crates/vm/src/builtins/dict.rs

---------

Co-authored-by: Jeong YunWon <jeong@youknowone.org>
Co-authored-by: Jeong, YunWon <69878+youknowone@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2026-10-14 16:42:56 +09:00
1 parent bef501fcd1
commit 28393e932d
40 files changed
+2705 -493

No files matched your search

Vendored
+25 -10
View File
@@ -100,14 +100,14 @@ def copy(x):
return _reconstruct(x, None, *rv)
_copy_atomic_types = {types.NoneType, int, float, bool, complex, str, tuple,
bytes, frozenset, type, range, slice, property,
_copy_atomic_types = frozenset({types.NoneType, int, float, bool, complex, str, tuple,
bytes, frozendict, frozenset, type, range, slice, property,
types.BuiltinFunctionType, types.EllipsisType,
types.NotImplementedType, types.FunctionType, types.CodeType,
weakref.ref, super}
_copy_builtin_containers = {list, dict, set, bytearray}
weakref.ref, super})
_copy_builtin_containers = frozenset({list, dict, set, bytearray})
def deepcopy(x, memo=None, _nil=[]):
def deepcopy(x, memo=None):
"""Deep copy operation on arbitrary Python objects.
See the module's __doc__ string for more info.
@@ -122,8 +122,8 @@ def deepcopy(x, memo=None, _nil=[]):
if memo is None:
memo = {}
else:
y = memo.get(d, _nil)
if y is not _nil:
y = memo.get(d, None)
if y is not None:
return y
copier = _deepcopy_dispatch.get(cls)
@@ -162,9 +162,9 @@ def deepcopy(x, memo=None, _nil=[]):
_keep_alive(x, memo) # Make sure x lives at least as long as d
return y
_atomic_types = {types.NoneType, types.EllipsisType, types.NotImplementedType,
_atomic_types = frozenset({types.NoneType, types.EllipsisType, types.NotImplementedType,
int, float, bool, complex, bytes, str, types.CodeType, type, range,
types.BuiltinFunctionType, types.FunctionType, weakref.ref, property}
types.BuiltinFunctionType, types.FunctionType, weakref.ref, property})
_deepcopy_dispatch = d = {}
@@ -203,6 +203,21 @@ def _deepcopy_dict(x, memo, deepcopy=deepcopy):
return y
d[dict] = _deepcopy_dict
def _deepcopy_frozendict(x, memo, deepcopy=deepcopy):
y = {}
for key, value in x.items():
y[deepcopy(key, memo)] = deepcopy(value, memo)
# We're not going to put the frozendict in the memo, but it's still
# important we check for it, in case the frozendict contains recursive
# mutable structures.
try:
return memo[id(x)]
except KeyError:
pass
return frozendict(y)
d[frozendict] = _deepcopy_frozendict
def _deepcopy_method(x, memo): # Copy instance methods
return type(x)(x.__func__, deepcopy(x.__self__, memo))
d[types.MethodType] = _deepcopy_method
@@ -230,7 +245,7 @@ def _reconstruct(x, memo, func, args,
*, deepcopy=deepcopy):
deep = memo is not None
if deep and args:
args = (deepcopy(arg, memo) for arg in args)
args = [deepcopy(arg, memo) for arg in args]
y = func(*args)
if deep:
memo[id(x)] = y
+70 -63
View File
@@ -4,7 +4,7 @@ import collections
from test import support
class BasicTestMappingProtocol(unittest.TestCase):
class BasicTestImmutableMappingProtocol(unittest.TestCase):
# This base class can be used to check that an object conforms to the
# mapping protocol
@@ -20,12 +20,9 @@ class BasicTestMappingProtocol(unittest.TestCase):
"""Return an empty mapping object"""
return self.type2test()
def _full_mapping(self, data):
"""Return a mapping object with the value contained in data
"""Return a mapping object with the values contained in data
dictionary"""
x = self._empty_mapping()
for key, value in data.items():
x[key] = value
return x
return self.type2test(data)
def __init__(self, *args, **kw):
unittest.TestCase.__init__(self, *args, **kw)
@@ -88,6 +85,72 @@ class BasicTestMappingProtocol(unittest.TestCase):
self.assertEqual(d.get(knownkey, knownvalue), knownvalue)
self.assertNotIn(knownkey, d)
def test_constructor(self):
self.assertEqual(self._empty_mapping(), self._empty_mapping())
def test_bool(self):
self.assertTrue(not self._empty_mapping())
self.assertTrue(self.reference)
self.assertFalse(bool(self._empty_mapping()))
self.assertTrue(bool(self.reference))
def test_keys(self):
d = self._empty_mapping()
self.assertEqual(list(d.keys()), [])
d = self.reference
self.assertIn(list(self.inmapping.keys())[0], d.keys())
self.assertNotIn(list(self.other.keys())[0], d.keys())
self.assertRaises(TypeError, d.keys, None)
def test_values(self):
d = self._empty_mapping()
self.assertEqual(list(d.values()), [])
self.assertRaises(TypeError, d.values, None)
def test_items(self):
d = self._empty_mapping()
self.assertEqual(list(d.items()), [])
self.assertRaises(TypeError, d.items, None)
def test_len(self):
d = self._empty_mapping()
self.assertEqual(len(d), 0)
def test_getitem(self):
d = self.reference
self.assertEqual(d[list(self.inmapping.keys())[0]],
list(self.inmapping.values())[0])
self.assertRaises(TypeError, d.__getitem__)
# no test_fromkeys or test_copy as both os.environ and selves don't support it
def test_get(self):
d = self._empty_mapping()
self.assertIsNone(d.get(list(self.other.keys())[0]))
self.assertEqual(d.get(list(self.other.keys())[0], 3), 3)
d = self.reference
self.assertIsNone(d.get(list(self.other.keys())[0]))
self.assertEqual(d.get(list(self.other.keys())[0], 3), 3)
self.assertEqual(d.get(list(self.inmapping.keys())[0]),
list(self.inmapping.values())[0])
self.assertEqual(d.get(list(self.inmapping.keys())[0], 3),
list(self.inmapping.values())[0])
self.assertRaises(TypeError, d.get)
self.assertRaises(TypeError, d.get, None, None, None)
class BasicTestMappingProtocol(BasicTestImmutableMappingProtocol):
def _full_mapping(self, data):
"""Return a mapping object with the values contained in data
dictionary"""
x = self._empty_mapping()
for key, value in data.items():
x[key] = value
return x
def test_write(self):
# Test for write operations on mapping
p = self._empty_mapping()
@@ -130,46 +193,6 @@ class BasicTestMappingProtocol(unittest.TestCase):
p=self._empty_mapping()
self.assertRaises(KeyError, p.popitem)
def test_constructor(self):
self.assertEqual(self._empty_mapping(), self._empty_mapping())
def test_bool(self):
self.assertTrue(not self._empty_mapping())
self.assertTrue(self.reference)
self.assertTrue(bool(self._empty_mapping()) is False)
self.assertTrue(bool(self.reference) is True)
def test_keys(self):
d = self._empty_mapping()
self.assertEqual(list(d.keys()), [])
d = self.reference
self.assertIn(list(self.inmapping.keys())[0], d.keys())
self.assertNotIn(list(self.other.keys())[0], d.keys())
self.assertRaises(TypeError, d.keys, None)
def test_values(self):
d = self._empty_mapping()
self.assertEqual(list(d.values()), [])
self.assertRaises(TypeError, d.values, None)
def test_items(self):
d = self._empty_mapping()
self.assertEqual(list(d.items()), [])
self.assertRaises(TypeError, d.items, None)
def test_len(self):
d = self._empty_mapping()
self.assertEqual(len(d), 0)
def test_getitem(self):
d = self.reference
self.assertEqual(d[list(self.inmapping.keys())[0]],
list(self.inmapping.values())[0])
self.assertRaises(TypeError, d.__getitem__)
def test_update(self):
# mapping argument
d = self._empty_mapping()
@@ -265,22 +288,6 @@ class BasicTestMappingProtocol(unittest.TestCase):
self.assertRaises(ValueError, d.update, [(1, 2, 3)])
# no test_fromkeys or test_copy as both os.environ and selves don't support it
def test_get(self):
d = self._empty_mapping()
self.assertTrue(d.get(list(self.other.keys())[0]) is None)
self.assertEqual(d.get(list(self.other.keys())[0], 3), 3)
d = self.reference
self.assertTrue(d.get(list(self.other.keys())[0]) is None)
self.assertEqual(d.get(list(self.other.keys())[0], 3), 3)
self.assertEqual(d.get(list(self.inmapping.keys())[0]),
list(self.inmapping.values())[0])
self.assertEqual(d.get(list(self.inmapping.keys())[0], 3),
list(self.inmapping.values())[0])
self.assertRaises(TypeError, d.get)
self.assertRaises(TypeError, d.get, None, None, None)
def test_setdefault(self):
d = self._empty_mapping()
self.assertRaises(TypeError, d.setdefault)
@@ -622,7 +629,7 @@ class TestHashMappingProtocol(TestMappingProtocol):
d = self._full_mapping({1: BadRepr()})
self.assertRaises(Exc, repr, d)
@support.run_with_limited_c_stack()
@support.skip_if_huge_c_stack()
@support.skip_wasi_stack_overflow()
@support.skip_emscripten_stack_overflow()
@support.skip_if_sanitizer("requires deep stack", ub=True)
-4
View File
@@ -4394,8 +4394,6 @@ class AbstractPickleTests:
def test_fast_save_enter_set(self):
self.fast_save_enter(lambda i: set([i]))
# TODO: RUSTPYTHON; frozendict support: https://github.com/RustPython/RustPython/pull/8951
@unittest.expectedFailure
def test_fast_save_enter_frozendict(self):
if self.py_version < (3, 15):
self.skipTest('need frozendict')
@@ -4443,8 +4441,6 @@ class AbstractPickleTests:
depth=FAST_NESTING_LIMIT+1,
compare_equal=False)
# TODO: RUSTPYTHON; frozendict support: https://github.com/RustPython/RustPython/pull/8951
@unittest.expectedFailure
def test_deep_nested_struct_frozendict(self):
if self.py_version < (3, 15):
self.skipTest('need frozendict')
+216 -13
View File
@@ -4,6 +4,7 @@ import ast
import builtins
import collections
import contextlib
import copy
import decimal
import fractions
import gc
@@ -21,6 +22,7 @@ import types
import typing
import unittest
import warnings
import weakref
from contextlib import ExitStack
from functools import partial
from inspect import CO_COROUTINE
@@ -31,6 +33,7 @@ from operator import neg
from test import support
from test.support import cpython_only, swap_attr
from test.support import async_yield, run_yielding_async_fn
from test.support import warnings_helper
from test.support.import_helper import import_module
from test.support.os_helper import (EnvironmentVarGuard, TESTFN, unlink)
from test.support.script_helper import assert_python_ok
@@ -51,6 +54,10 @@ HAVE_DOUBLE_ROUNDING = (x + y == 1e16 + 4)
# used as proof of globals being used
A_GLOBAL_VALUE = 123
A_SENTINEL = sentinel("A_SENTINEL")
class SentinelContainer:
CLASS_SENTINEL = sentinel("SentinelContainer.CLASS_SENTINEL")
class Squares:
@@ -245,7 +252,7 @@ class BuiltinTest(ComplexesAreIdenticalMixin, unittest.TestCase):
S = [10, 20, 30]
self.assertEqual(any(x > 42 for x in S), False)
def test_all_any_tuple_optimization(self):
def test_all_any_tuple_list_set_optimization(self):
def f_all():
return all(x-2 for x in [1,2,3])
@@ -255,7 +262,13 @@ class BuiltinTest(ComplexesAreIdenticalMixin, unittest.TestCase):
def f_tuple():
return tuple(2*x for x in [1,2,3])
funcs = [f_all, f_any, f_tuple]
def f_list():
return list(2*x for x in [1,2,3])
def f_set():
return set(2*x for x in [1,2,3])
funcs = [f_all, f_any, f_tuple, f_list, f_set]
for f in funcs:
# check that generator code object is not duplicated
@@ -265,33 +278,35 @@ class BuiltinTest(ComplexesAreIdenticalMixin, unittest.TestCase):
# check the overriding the builtins works
global all, any, tuple
saved = all, any, tuple
global all, any, tuple, list, set
saved = all, any, tuple, list, set
try:
all = lambda x : "all"
any = lambda x : "any"
tuple = lambda x : "tuple"
list = lambda x : "list"
set = lambda x : "set"
overridden_outputs = [f() for f in funcs]
finally:
all, any, tuple = saved
self.assertEqual(overridden_outputs, ['all', 'any', 'tuple'])
all, any, tuple, list, set = saved
self.assertEqual(overridden_outputs, ['all', 'any', 'tuple', 'list', 'set'])
# Now repeat, overriding the builtins module as well
saved = all, any, tuple
saved = all, any, tuple, list, set
try:
builtins.all = all = lambda x : "all"
builtins.any = any = lambda x : "any"
builtins.tuple = tuple = lambda x : "tuple"
builtins.list = list = lambda x : "list"
builtins.set = set = lambda x : "set"
overridden_outputs = [f() for f in funcs]
finally:
all, any, tuple = saved
builtins.all, builtins.any, builtins.tuple = saved
self.assertEqual(overridden_outputs, ['all', 'any', 'tuple'])
all, any, tuple, list, set = saved
builtins.all, builtins.any, builtins.tuple, builtins.list, builtins.set = saved
self.assertEqual(overridden_outputs, ['all', 'any', 'tuple', 'list', 'set'])
def test_builtin_call_async_genexpr_no_crash(self):
async def f_all():
@@ -527,6 +542,10 @@ class BuiltinTest(ComplexesAreIdenticalMixin, unittest.TestCase):
'''a = [x async for x in (x async for x in arange(5))][1]''',
'''a, = [1 for x in {x async for x in arange(1)}]''',
'''a = [await sleep(0, x) async for x in arange(2)][1]''',
'''a = [await sleep(0, 1) for _ in [0]][0]''',
'''a = {await sleep(0, 1) for _ in [0]}.pop()''',
'''a = {0: await sleep(0, 1) for _ in [0]}[0]''',
'''a = (lambda x=[await sleep(0, 1) for _ in [0]]: x)()[0]''',
# gh-121637: Make sure we correctly handle the case where the
# async code is optimized away
'''assert not await sleep(0); a = 1''',
@@ -595,7 +614,26 @@ class BuiltinTest(ComplexesAreIdenticalMixin, unittest.TestCase):
'''def f():
async with Lock() as l:
a = 1
'''
''',
'''class C:
[await x for x in y]
''',
'''class C:
[x async for x in arange(10)]
''',
'''async def f():
class C:
[await x for x in y]
''',
'''lambda: [await x for x in y]''',
'''class C:
def f(self, x=[await y for y in z]):
pass
''',
'''type T = [await x for x in y]''',
'''async def f[T=[await x for x in y]]():
pass
''',
]
for mode, code_sample in product(modes, code_samples):
source = dedent(code_sample)
@@ -796,6 +834,16 @@ class BuiltinTest(ComplexesAreIdenticalMixin, unittest.TestCase):
raise ValueError
self.assertRaises(ValueError, eval, "foo", {}, X())
def test_eval_frozendict(self):
ns = frozendict(x=1, data=[], __builtins__=__builtins__)
eval("data.append(x)", ns, ns)
self.assertEqual(ns['data'], [1])
ns = frozendict()
errmsg = "cannot assign __builtins__ to frozendict globals"
with self.assertRaisesRegex(TypeError, errmsg):
eval("", ns, ns)
def test_eval_kwargs(self):
data = {"A_GLOBAL_VALUE": 456}
self.assertEqual(eval("globals()['A_GLOBAL_VALUE']", globals=data), 456)
@@ -894,6 +942,21 @@ class BuiltinTest(ComplexesAreIdenticalMixin, unittest.TestCase):
del l['__builtins__']
self.assertEqual((g, l), ({'a': 1}, {'b': 2}))
def test_exec_frozendict(self):
ns = frozendict(x=1, data=[], __builtins__=__builtins__)
exec("data.append(x)", ns, ns)
self.assertEqual(ns['data'], [1])
ns = frozendict(__builtins__=__builtins__)
errmsg = "'frozendict' object does not support item assignment"
with self.assertRaisesRegex(TypeError, errmsg):
exec("x = 1", ns, ns)
ns = frozendict()
errmsg = "cannot assign __builtins__ to frozendict globals"
with self.assertRaisesRegex(TypeError, errmsg):
exec("", ns, ns)
def test_exec_kwargs(self):
g = {}
exec('global z\nz = 1', globals=g)
@@ -1108,6 +1171,30 @@ class BuiltinTest(ComplexesAreIdenticalMixin, unittest.TestCase):
three_freevars.__globals__,
closure=my_closure)
@unittest.expectedFailure # TODO: RUSTPYTHON; SyntaxError: invalid decimal literal
def test_exec_filter_syntax_warnings_by_module(self):
filename = support.findfile('test_import/data/syntax_warnings.py')
with open(filename, 'rb') as f:
source = f.read()
with warnings.catch_warnings(record=True) as wlog:
warnings.simplefilter('error')
warnings.filterwarnings('always', module=r'<string>\z')
exec(source, {})
self.assertEqual(sorted(wm.lineno for wm in wlog), [4, 7, 10, 13, 14, 21])
for wm in wlog:
self.assertEqual(wm.filename, '<string>')
self.assertIs(wm.category, SyntaxWarning)
with warnings.catch_warnings(record=True) as wlog:
warnings.simplefilter('error')
warnings.filterwarnings('always', module=r'package.module\z')
warnings.filterwarnings('error', module=r'<string>')
exec(source, {'__name__': 'package.module', '__file__': filename})
self.assertEqual(sorted(wm.lineno for wm in wlog), [4, 7, 10, 13, 14, 21])
for wm in wlog:
self.assertEqual(wm.filename, '<string>')
self.assertIs(wm.category, SyntaxWarning)
def test_filter(self):
self.assertEqual(list(filter(lambda c: 'a' <= c <= 'z', 'Hello World')), list('elloorld'))
@@ -1868,6 +1955,115 @@ class BuiltinTest(ComplexesAreIdenticalMixin, unittest.TestCase):
__repr__ = None
self.assertRaises(TypeError, repr, C())
def test_sentinel(self):
missing = sentinel("MISSING")
other = sentinel("MISSING")
self.assertIsInstance(missing, sentinel)
self.assertIs(type(missing), sentinel)
self.assertEqual(missing.__name__, "MISSING")
self.assertEqual(missing.__module__, __name__)
self.assertIsNot(missing, other)
self.assertEqual(repr(missing), "MISSING")
self.assertTrue(missing)
self.assertIs(copy.copy(missing), missing)
self.assertIs(copy.deepcopy(missing), missing)
self.assertEqual(missing, missing)
self.assertNotEqual(missing, other)
self.assertRaises(TypeError, sentinel)
self.assertRaises(TypeError, sentinel, "MISSING", "EXTRA")
self.assertRaises(TypeError, sentinel, name="MISSING")
with self.assertRaisesRegex(TypeError, "must be str"):
sentinel(1)
self.assertTrue(sentinel.__flags__ & support._TPFLAGS_IMMUTABLETYPE)
self.assertTrue(sentinel.__flags__ & support._TPFLAGS_HAVE_GC)
self.assertFalse(sentinel.__flags__ & support._TPFLAGS_BASETYPE)
with self.assertRaises(TypeError):
class SubSentinel(sentinel):
pass
def test_sentinel_attributes(self):
missing = sentinel("MISSING")
with self.assertRaises(TypeError):
sentinel.attribute = "value"
with self.assertRaises(AttributeError):
missing.attribute = "value"
with self.assertRaises(AttributeError):
missing.__name__ = "CHANGED"
missing.__module__ = "changed"
self.assertEqual(missing.__module__, "changed")
with self.assertRaises(AttributeError):
del missing.__name__
del missing.__module__
with self.assertRaises(AttributeError):
missing.__module__
def test_sentinel_repr(self):
with_repr = sentinel("WITH_REPR", repr="custom")
without_repr = sentinel("WITHOUT_REPR", repr=None)
self.assertEqual(repr(with_repr), "custom")
self.assertEqual(repr(without_repr), "WITHOUT_REPR")
self.assertEqual(str(with_repr), "custom")
self.assertEqual(str(without_repr), "WITHOUT_REPR")
with self.assertRaisesRegex(TypeError, "repr.*str or None"):
sentinel("BAD_REPR", repr=42)
def test_sentinel_pickle(self):
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
with self.subTest(protocol=proto):
self.assertIs(
pickle.loads(pickle.dumps(A_SENTINEL, protocol=proto)),
A_SENTINEL)
self.assertIs(
pickle.loads(pickle.dumps(
SentinelContainer.CLASS_SENTINEL, protocol=proto)),
SentinelContainer.CLASS_SENTINEL)
missing = sentinel("MISSING")
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
with self.subTest(protocol=proto):
with self.assertRaises(pickle.PicklingError):
pickle.dumps(missing, protocol=proto)
def test_sentinel_str_subclass_name_cycle(self):
class Name(str):
pass
name = Name("MISSING")
missing = sentinel(name)
self.assertIs(missing.__name__, name)
self.assertTrue(gc.is_tracked(missing))
name.missing = missing
ref = weakref.ref(name)
del name, missing
support.gc_collect()
self.assertIsNone(ref())
def test_sentinel_union(self):
missing = sentinel("MISSING")
self.assertIsInstance(missing | int, typing.Union)
self.assertEqual((missing | int).__args__, (missing, int))
self.assertIsInstance(int | missing, typing.Union)
self.assertEqual((int | missing).__args__, (int, missing))
self.assertIs(missing | missing, missing)
self.assertEqual(repr(int | missing), "int | MISSING")
self.assertIsInstance(missing | None, typing.Union)
self.assertEqual((missing | None).__args__, (missing, type(None)))
self.assertIsInstance(None | missing, typing.Union)
self.assertEqual((None | missing).__args__, (type(None), missing))
self.assertIsInstance(missing | list[int], typing.Union)
self.assertEqual((missing | list[int]).__args__, (missing, list[int]))
self.assertIsInstance(missing | (int | str), typing.Union)
self.assertEqual((missing | (int | str)).__args__, (missing, int, str))
with self.assertRaises(TypeError):
missing | 1
with self.assertRaises(TypeError):
1 | missing
def test_round(self):
self.assertEqual(round(0.0), 0.0)
self.assertEqual(type(round(0.0)), int)
@@ -2594,6 +2790,7 @@ class PtyTests(unittest.TestCase):
finally:
signal.signal(signal.SIGHUP, old_sighup)
@warnings_helper.ignore_fork_in_thread_deprecation_warnings()
def _run_child(self, child, terminal_input):
r, w = os.pipe() # Pipe test results from child back to parent
try:
@@ -2983,6 +3180,12 @@ class TestType(unittest.TestCase):
A.__doc__ = doc
self.assertEqual(A.__doc__, doc)
def test_type_frozendict(self):
A = type('A', (), frozendict({'x': 4, 'y': 2}))
self.assertEqual(A.x, 4)
self.assertEqual(A.y, 2)
self.assertEqual(A.__name__, 'A')
def test_bad_args(self):
with self.assertRaises(TypeError):
type()
+44 -2
View File
@@ -60,7 +60,7 @@ class TestCopy(unittest.TestCase):
def __reduce_ex__(self, proto):
c.append(1)
return ""
def __reduce__(self):
def __reduce__(*args):
self.fail("shouldn't call this")
c = []
x = C()
@@ -70,9 +70,15 @@ class TestCopy(unittest.TestCase):
def test_copy_reduce(self):
class C(object):
def __reduce_ex__(*args):
self.fail("shouldn't call this")
def __reduce__(self):
c.append(1)
return ""
def __getattribute__(self, name):
if name == "__reduce_ex__":
raise AttributeError(name)
return object.__getattribute__(self, name)
c = []
x = C()
y = copy.copy(x)
@@ -133,6 +139,12 @@ class TestCopy(unittest.TestCase):
self.assertEqual(y, x)
self.assertIsNot(y, x)
def test_copy_frozendict(self):
x = frozendict(x=1, y=2)
self.assertIs(copy.copy(x), x)
x = frozendict()
self.assertIs(copy.copy(x), x)
def test_copy_set(self):
x = {1, 2, 3}
y = copy.copy(x)
@@ -323,7 +335,7 @@ class TestCopy(unittest.TestCase):
def __reduce_ex__(self, proto):
c.append(1)
return ""
def __reduce__(self):
def __reduce__(*args):
self.fail("shouldn't call this")
c = []
x = C()
@@ -333,9 +345,15 @@ class TestCopy(unittest.TestCase):
def test_deepcopy_reduce(self):
class C(object):
def __reduce_ex__(*args):
self.fail("shouldn't call this")
def __reduce__(self):
c.append(1)
return ""
def __getattribute__(self, name):
if name == "__reduce_ex__":
raise AttributeError(name)
return object.__getattribute__(self, name)
c = []
x = C()
y = copy.deepcopy(x)
@@ -421,6 +439,30 @@ class TestCopy(unittest.TestCase):
self.assertIsNot(x, y)
self.assertIsNot(x["foo"], y["foo"])
def test_deepcopy_frozendict(self):
x = frozendict({"foo": [1, 2], "bar": 3})
y = copy.deepcopy(x)
self.assertEqual(y, x)
self.assertIsNot(x, y)
self.assertIsNot(x["foo"], y["foo"])
# recursive frozendict
x = frozendict(foo=[])
x['foo'].append(x)
y = copy.deepcopy(x)
self.assertEqual(y.keys(), x.keys())
self.assertIsNot(x, y)
self.assertIsNot(x["foo"], y["foo"])
self.assertIs(y['foo'][0], y)
x = frozendict(foo=[])
x['foo'].append(x)
x = x['foo']
y = copy.deepcopy(x)
self.assertIsNot(x, y)
self.assertIsNot(x[0], y[0])
self.assertIs(y[0]['foo'], y)
@support.skip_if_huge_c_stack()
@support.skip_emscripten_stack_overflow()
@support.skip_wasi_stack_overflow()
+272 -4
View File
@@ -678,7 +678,7 @@ class DictTest(unittest.TestCase):
d = {1: BadRepr()}
self.assertRaises(Exc, repr, d)
@support.run_with_limited_c_stack()
@support.skip_if_huge_c_stack()
@support.skip_wasi_stack_overflow()
@support.skip_emscripten_stack_overflow()
def test_repr_deep(self):
@@ -951,7 +951,6 @@ class DictTest(unittest.TestCase):
'f': None, 'g': None, 'h': None}
d = {}
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_container_iterator(self):
# Bug #3680: tp_traverse was not implemented for dictiter and
# dictview objects.
@@ -1264,7 +1263,6 @@ class DictTest(unittest.TestCase):
support.check_free_after_iterating(self, lambda d: iter(d.values()), dict)
support.check_free_after_iterating(self, lambda d: iter(d.items()), dict)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_equal_operator_modifying_operand(self):
# test fix for seg fault reported in bpo-27945 part 3.
class X():
@@ -1426,6 +1424,32 @@ class DictTest(unittest.TestCase):
for it in iterators:
self.assertEqual(list(it), [])
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: RuntimeError not raised
def test_reversed_dict_keys_changed_during_iteration(self):
d = dict.fromkeys(range(10))
for i in range(7):
del d[i]
iterators = (
reversed(d),
reversed(d.keys()),
reversed(d.values()),
reversed(d.items()),
)
for it in iterators:
next(it)
# Same size as before, but with different keys below
# the iterators' current position.
d.clear()
d.update(dict.fromkeys(range(10)))
for i in range(3, 10):
del d[i]
for it in iterators:
with self.assertRaisesRegex(RuntimeError, 'keys changed'):
list(it)
def test_dict_copy_order(self):
# bpo-34320
od = collections.OrderedDict([('a', 1), ('b', 2)])
@@ -1634,7 +1658,7 @@ class DictTest(unittest.TestCase):
with check_unhashable_key():
d.get(key)
# Only TypeError exception is overriden,
# Only TypeError exception is overridden,
# other exceptions are left unchanged.
class HashError:
def __hash__(self):
@@ -1830,6 +1854,250 @@ class Dict(dict):
class SubclassMappingTests(mapping_tests.BasicTestMappingProtocol):
type2test = Dict
class FrozenDictMappingTests(mapping_tests.BasicTestImmutableMappingProtocol):
type2test = frozendict
class FrozenDict(frozendict):
pass
class FrozenDictSlots(frozendict):
__slots__ = ('slot_attr',)
class FrozenDictTests(unittest.TestCase):
def test_constructor(self):
# frozendict.__init__() has no effect
d = frozendict(a=1, b=2, c=3)
d.__init__(x=1)
self.assertEqual(d, frozendict(a=1, b=2, c=3))
# dict constructor cannot be used on frozendict
with self.assertRaises(TypeError):
dict.__init__(d, x=1)
# Avoid copy if it's frozendict type
d2 = frozendict(d)
self.assertIs(d2, d)
d2 = FrozenDict(d)
self.assertIsNot(d2, d)
self.assertEqual(d2, d)
def test_copy(self):
d = frozendict(x=1, y=2)
d2 = d.copy()
self.assertIs(d2, d)
d = FrozenDict(x=1, y=2)
d2 = d.copy()
self.assertIsNot(d2, d)
self.assertEqual(d2, frozendict(x=1, y=2))
self.assertEqual(type(d2), frozendict)
def test_merge(self):
# test "a | b" operator
self.assertEqual(frozendict(x=1) | frozendict(y=2),
frozendict({'x': 1, 'y': 2}))
self.assertEqual(frozendict(x=1) | dict(y=2),
frozendict({'x': 1, 'y': 2}))
self.assertEqual(frozendict(x=1, y=2) | frozendict(y=5),
frozendict({'x': 1, 'y': 5}))
self.assertEqual(FrozenDict(x=1, y=2) | FrozenDict(y=5),
frozendict({'x': 1, 'y': 5}))
fd = frozendict(x=1, y=2)
self.assertIs(fd | frozendict(), fd)
self.assertIs(fd | {}, fd)
self.assertIs(frozendict() | fd, fd)
fd = FrozenDict(x=1, y=2)
self.assertEqual(fd | frozendict(), fd)
self.assertEqual(fd | {}, fd)
self.assertEqual(frozendict() | fd, fd)
# gh-149676: Test hash(frozendict | frozendict)
a = frozendict({"a": 1})
b = frozendict({"b": 2})
self.assertEqual(hash(a | b), hash(frozendict({"a": 1, "b": 2})))
def test_update(self):
# test "a |= b" operator
d = frozendict(x=1)
copy = d
self.assertIs(copy, d)
d |= frozendict(y=2)
self.assertIsNot(copy, d)
self.assertEqual(d, frozendict({'x': 1, 'y': 2}))
self.assertEqual(copy, frozendict({'x': 1}))
def test_items_xor(self):
# test "a ^ b" operator on items views
res = frozendict(a=1, b=2).items() ^ frozendict(b=2, c=3).items()
self.assertEqual(res, {('a', 1), ('c', 3)})
def test_repr(self):
d = frozendict()
self.assertEqual(repr(d), "frozendict()")
d = frozendict(x=1, y=2)
self.assertEqual(repr(d), "frozendict({'x': 1, 'y': 2})")
d = FrozenDict(x=1, y=2)
self.assertEqual(repr(d), "FrozenDict({'x': 1, 'y': 2})")
def test_hash(self):
# hash() doesn't rely on the items order
self.assertEqual(hash(frozendict(x=1, y=2)),
hash(frozendict(y=2, x=1)))
# Check that hash() computes the hash of (key, value) pairs
cases = [
frozendict(a=False, b=True, c=True),
frozendict(a=True, b=False, c=True),
frozendict(a=True, b=True, c=False),
frozendict({False: "a", "b": True, "c": True}),
frozendict({"a": "b", False: True, True: "c"}),
]
hashes = {hash(fd) for fd in cases}
self.assertEqual(len(hashes), len(cases))
fd = frozendict(x=[1], y=[2])
with self.assertRaisesRegex(TypeError, "unhashable type: 'list'"):
hash(fd)
@support.cpython_only
def test_hash_cpython(self):
# Check that hash(frozendict) implementation is:
# hash(frozenset(fd.items()))
for fd in (
frozendict(),
frozendict(x=1, y=2),
frozendict(y=2, x=1),
frozendict(a=False, b=True, c=True),
frozendict.fromkeys('abc'),
):
with self.subTest(fd=fd):
self.assertEqual(hash(fd), hash(frozenset(fd.items())))
def test_fromkeys(self):
self.assertEqual(frozendict.fromkeys('abc'),
frozendict(a=None, b=None, c=None))
# Subclass which overrides the constructor
created = frozendict(x=1)
class FrozenDictSubclass(frozendict):
def __new__(cls, *args, **kwargs):
if args or kwargs:
return super().__new__(cls, *args, **kwargs)
else:
return created
fd = FrozenDictSubclass.fromkeys("abc")
self.assertEqual(fd, frozendict(x=1, a=None, b=None, c=None))
self.assertEqual(type(fd), FrozenDictSubclass)
self.assertEqual(created, frozendict(x=1))
fd = FrozenDictSubclass.fromkeys(frozendict(y=2))
self.assertEqual(fd, frozendict(x=1, y=None))
self.assertEqual(type(fd), FrozenDictSubclass)
self.assertEqual(created, frozendict(x=1))
# Dict subclass with a constructor which returns a frozendict
# by default
class DictSubclass(dict):
def __new__(cls, *args, **kwargs):
if args or kwargs:
return super().__new__(cls, *args, **kwargs)
else:
return created
fd = DictSubclass.fromkeys("abc")
self.assertEqual(fd, frozendict(x=1, a=None, b=None, c=None))
self.assertEqual(type(fd), DictSubclass)
self.assertEqual(created, frozendict(x=1))
# Subclass which doesn't override the constructor
class FrozenDictSubclass2(frozendict):
pass
fd = FrozenDictSubclass2.fromkeys("abc")
self.assertEqual(fd, frozendict(a=None, b=None, c=None))
self.assertEqual(type(fd), FrozenDictSubclass2)
def test_pickle(self):
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
for fd in (
frozendict(),
frozendict(x=1, y=2),
FrozenDict(x=1, y=2),
FrozenDictSlots(x=1, y=2),
):
if type(fd) == FrozenDict:
fd.attr = 123
if type(fd) == FrozenDictSlots:
fd.slot_attr = 456
with self.subTest(fd=fd, proto=proto):
if proto >= 2:
p = pickle.dumps(fd, proto)
fd2 = pickle.loads(p)
self.assertEqual(fd2, fd)
self.assertEqual(type(fd2), type(fd))
if type(fd) == FrozenDict:
self.assertEqual(fd2.attr, 123)
if type(fd) == FrozenDictSlots:
self.assertEqual(fd2.slot_attr, 456)
else:
# protocol 0 and 1 don't support frozendict
with self.assertRaises(TypeError):
pickle.dumps(fd, proto)
def test_pickle_iter(self):
fd = frozendict(c=1, b=2, a=3, d=4, e=5, f=6)
for method_name in (None, 'keys', 'values', 'items'):
if method_name is not None:
meth = getattr(fd, method_name)
else:
meth = lambda: fd
expected = list(meth())[1:]
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
with self.subTest(method_name=method_name, protocol=proto):
it = iter(meth())
next(it)
p = pickle.dumps(it, proto)
unpickled = pickle.loads(p)
self.assertEqual(list(unpickled), expected)
self.assertEqual(list(it), expected)
def test_unhashable_key(self):
d = frozendict(a=1)
key = [1, 2, 3]
def check_unhashable_key():
msg = "cannot use 'list' as a frozendict key (unhashable type: 'list')"
return self.assertRaisesRegex(TypeError, re.escape(msg))
with check_unhashable_key():
key in d
with check_unhashable_key():
d[key]
with check_unhashable_key():
d.get(key)
# Only TypeError exception is overridden,
# other exceptions are left unchanged.
class HashError:
def __hash__(self):
raise KeyError('error')
key2 = HashError()
with self.assertRaises(KeyError):
key2 in d
with self.assertRaises(KeyError):
d[key2]
with self.assertRaises(KeyError):
d.get(key2)
if __name__ == "__main__":
unittest.main()
+1 -1
View File
@@ -742,7 +742,7 @@ plain ol' Python and is guaranteed to be available.
>>> import builtins
>>> tests = doctest.DocTestFinder().find(builtins)
>>> 750 < len(tests) < 800 # approximate number of objects with docstrings
>>> 750 < len(tests) < 850 # approximate number of objects with docstrings
True
>>> real_tests = [t for t in tests if len(t.examples) > 0]
>>> len(real_tests) # objects that actually have doctests
+1 -1
View File
@@ -135,7 +135,7 @@ class RebindBuiltinsTests(unittest.TestCase):
self.assertEqual(foo(), 7)
@unittest.expectedFailure # TODO: RUSTPYTHON
@unittest.expectedFailureIf(sys.getrecursionlimit() == 256, "TODO: RUSTPYTHON; default debug recursion limit is 256")
@skip_wasi_stack_overflow()
def test_load_global_specialization_failure_keeps_oparg(self):
# https://github.com/python/cpython/issues/91625
+21
View File
@@ -0,0 +1,21 @@
# Syntax warnings emitted in different parts of the Python compiler.
# Parser/lexer/lexer.c
x = 1or 0 # line 4
# Parser/tokenizer/helpers.c
'\z' # line 7
# Parser/string_parser.c
'\400' # line 10
# _PyCompile_Warn() in Python/codegen.c
assert(x, 'message') # line 13
x is 1 # line 14
# _PyErr_EmitSyntaxWarning() in Python/ast_preprocess.c
def f():
try:
pass
finally:
return 42 # line 21
+12
View File
@@ -107,6 +107,10 @@ class PyPicklerTests(AbstractPickleTests, unittest.TestCase):
def test_c_methods(self):
return super().test_c_methods()
@unittest.expectedFailureIf(sys.getrecursionlimit() == 256, "TODO: RUSTPYTHON; default debug recursion limit is 256")
def test_deep_nested_struct_frozendict(self):
return super().test_deep_nested_struct_frozendict()
@@ -408,6 +412,10 @@ if has_c_implementation:
pickler = _pickle.Pickler
unpickler = _pickle.Unpickler
@unittest.expectedSuccess # TODO: RUSTPYTHON; native pickler passes at the default debug recursion limit
def test_deep_nested_struct_frozendict(self):
return super().test_deep_nested_struct_frozendict()
class CPersPicklerTests(PyPersPicklerTests):
pickler = _pickle.Pickler
unpickler = _pickle.Unpickler
@@ -421,6 +429,10 @@ if has_c_implementation:
pickler = _pickle.Pickler
unpickler = pickle._Unpickler
@unittest.expectedSuccess # TODO: RUSTPYTHON; native pickler passes at the default debug recursion limit
def test_deep_nested_struct_frozendict(self):
return super().test_deep_nested_struct_frozendict()
class DumpPickle_CLoadPickle(PyPicklerTests):
pickler = pickle._Pickler
unpickler = _pickle.Unpickler
-1
View File
@@ -45,7 +45,6 @@ class AnyTest(unittest.TestCase):
mock.assert_called_with(ANY, foo=ANY)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_any_mock_calls_comparison_order(self):
mock = Mock()
class Foo(object):
+9 -4
View File
@@ -3,10 +3,10 @@ use crate::pystate::with_vm;
use crate::unicodeobject::decode_fsdefault_and_size;
use crate::util::{CStrExt, FfiPtrExt};
use core::ffi::{CStr, c_char, c_int};
use rustpython_vm::builtins::{PyCode, PyDict};
use rustpython_vm::builtins::{PyAnyDict, PyCode};
use rustpython_vm::function::ArgMapping;
use rustpython_vm::scope::Scope;
use rustpython_vm::{AsObject, PyObject, TryFromObject};
use rustpython_vm::{AsObject, PyObject, PyRef, TryFromObject};
use rustpython_vm::{PyObjectRef, version};
#[unsafe(no_mangle)]
@@ -32,12 +32,17 @@ pub unsafe extern "C" fn PyEval_EvalCode(
) -> *mut PyObject {
with_vm(|vm| {
let code = unsafe { co.assume_borrowed_and_cast::<PyCode>(vm) }?;
let globals = unsafe { globals.assume_borrowed_and_cast::<PyDict>(vm) }?;
let globals = PyRef::<PyAnyDict>::try_from_object(
vm,
unsafe { globals.assume_borrowed() }.to_owned(),
)?;
let locals = unsafe { locals.assume_borrowed_or_opt() }
.map(|obj| ArgMapping::try_from_object(vm, obj.to_owned()))
.transpose()?;
let scope = Scope::with_builtins(locals, globals.to_owned(), vm);
let locals = locals.or_else(|| Some(ArgMapping::from_anydict_exact(globals.clone())));
// Unlike Python's eval/exec, this API does not insert __builtins__.
let scope = Scope::new(locals, globals);
vm.run_code_obj(code.to_owned(), scope)
})
+78 -36
View File
@@ -4,9 +4,8 @@ use crate::pystate::with_vm;
use crate::util::{CStrExt, FfiPtrExt};
use core::ffi::{c_char, c_int};
use core::ptr::NonNull;
use rustpython_vm::AsObject;
use rustpython_vm::PyPayload;
use rustpython_vm::builtins::PyDict;
use rustpython_vm::builtins::{PyAnyDict, PyDict, PyFrozenDict};
use rustpython_vm::{AsObject, Py, PyPayload, PyResult, VirtualMachine};
define_py_check!(fn PyDict_Check, types.dict_type);
define_py_check!(exact fn PyDict_CheckExact, types.dict_type);
@@ -14,6 +13,27 @@ define_py_check!(fn PyDictKeys_Check, types.dict_keys_type);
define_py_check!(fn PyDictValues_Check, types.dict_values_type);
define_py_check!(fn PyDictItems_Check, types.dict_items_type);
fn any_dict<'a>(dict: &'a PyObject, vm: &VirtualMachine) -> PyResult<&'a PyAnyDict> {
PyAnyDict::from_object(dict)
.ok_or_else(|| vm.new_system_error("bad argument to internal function"))
}
fn writable_dict<'a>(
dict: &'a PyObject,
operation: &str,
vm: &VirtualMachine,
) -> PyResult<&'a Py<PyDict>> {
dict.downcast_ref::<PyDict>().ok_or_else(|| {
if dict.downcast_ref::<PyFrozenDict>().is_some() {
vm.new_type_error(format!(
"frozendict object does not support item {operation}"
))
} else {
vm.new_system_error("bad argument to internal function")
}
})
}
#[unsafe(no_mangle)]
pub extern "C" fn PyDict_New() -> *mut PyObject {
with_vm(|vm| vm.ctx.new_dict())
@@ -21,10 +41,10 @@ pub extern "C" fn PyDict_New() -> *mut PyObject {
#[unsafe(no_mangle)]
pub unsafe extern "C" fn PyDict_Clear(dict: *mut PyObject) {
with_vm(|vm| {
let dict = unsafe { dict.assume_borrowed_and_cast::<PyDict>(vm) }?;
dict.clear();
Ok(())
with_vm(|_vm| {
if let Some(dict) = unsafe { dict.assume_borrowed() }.downcast_ref::<PyDict>() {
dict.clear();
}
})
}
@@ -35,7 +55,7 @@ pub unsafe extern "C" fn PyDict_SetItem(
val: *mut PyObject,
) -> c_int {
with_vm(|vm| {
let dict = unsafe { dict.assume_borrowed_and_cast::<PyDict>(vm) }?;
let dict = writable_dict(unsafe { dict.assume_borrowed() }, "assignment", vm)?;
let key = unsafe { key.assume_borrowed() };
let value = unsafe { val.assume_borrowed() }.to_owned();
dict.inner_setitem(key, value, vm)
@@ -49,8 +69,8 @@ pub unsafe extern "C" fn PyDict_SetItemString(
val: *mut PyObject,
) -> c_int {
with_vm(|vm| {
let dict = unsafe { dict.assume_borrowed_and_cast::<PyDict>(vm) }?;
let key = unsafe { key.try_as_str(vm) }?;
let dict = writable_dict(unsafe { dict.assume_borrowed() }, "assignment", vm)?;
let value = unsafe { val.assume_borrowed() }.to_owned();
dict.inner_setitem(key, value, vm)
})
@@ -59,12 +79,14 @@ pub unsafe extern "C" fn PyDict_SetItemString(
#[unsafe(no_mangle)]
pub unsafe extern "C" fn PyDict_GetItem(dict: *mut PyObject, key: *mut PyObject) -> *mut PyObject {
with_vm(|vm| {
let dict = unsafe { dict.assume_borrowed_and_cast::<PyDict>(vm) }?;
let Some(dict) = PyAnyDict::from_object(unsafe { dict.assume_borrowed() }) else {
return core::ptr::null_mut();
};
let key = unsafe { key.assume_borrowed() };
match dict.inner_getitem_opt(key, vm) {
Ok(Some(value)) => Ok(value.as_object().as_raw().cast_mut()),
Ok(None) | Err(_) => Ok(core::ptr::null_mut()),
Ok(Some(value)) => value.as_object().as_raw().cast_mut(),
Ok(None) | Err(_) => core::ptr::null_mut(),
}
})
}
@@ -75,7 +97,7 @@ pub unsafe extern "C" fn PyDict_GetItemWithError(
key: *mut PyObject,
) -> *mut PyObject {
with_vm(|vm| {
let dict = unsafe { dict.assume_borrowed_and_cast::<PyDict>(vm) }?;
let dict = any_dict(unsafe { dict.assume_borrowed() }, vm)?;
let key = unsafe { key.assume_borrowed() };
if let Some(value) = dict.inner_getitem_opt(key, vm)? {
@@ -92,12 +114,16 @@ pub unsafe extern "C" fn PyDict_GetItemString(
key: *const c_char,
) -> *mut PyObject {
with_vm(|vm| {
let dict = unsafe { dict.assume_borrowed_and_cast::<PyDict>(vm) }?;
let key = unsafe { key.try_as_str(vm) }?;
let Ok(key) = (unsafe { key.try_as_str(vm) }) else {
return core::ptr::null_mut();
};
let Some(dict) = PyAnyDict::from_object(unsafe { dict.assume_borrowed() }) else {
return core::ptr::null_mut();
};
match dict.inner_getitem_opt(key, vm)? {
Some(value) => Ok(value.as_object().as_raw().cast_mut()),
None => Ok(core::ptr::null_mut()),
match dict.inner_getitem_opt(key, vm) {
Ok(Some(value)) => value.as_object().as_raw().cast_mut(),
Ok(None) | Err(_) => core::ptr::null_mut(),
}
})
}
@@ -112,8 +138,8 @@ pub unsafe extern "C" fn PyDict_GetItemStringRef(
unsafe {
*result = core::ptr::null_mut();
}
let dict = unsafe { dict.assume_borrowed_and_cast::<PyDict>(vm) }?;
let key = unsafe { key.try_as_str(vm) }?;
let dict = any_dict(unsafe { dict.assume_borrowed() }, vm)?;
if let Some(value) = dict.inner_getitem_opt(key, vm)? {
unsafe {
@@ -134,7 +160,7 @@ pub unsafe extern "C" fn PyDict_GetItemRef(
) -> c_int {
with_vm(|vm| {
unsafe { *result = core::ptr::null_mut() };
let dict = unsafe { dict.assume_borrowed_and_cast::<PyDict>(vm) }?;
let dict = any_dict(unsafe { dict.assume_borrowed() }, vm)?;
let key = unsafe { key.assume_borrowed() };
if let Some(value) = dict.inner_getitem_opt(key, vm)? {
@@ -165,7 +191,7 @@ pub unsafe extern "C" fn PyDict_SetDefaultRef(
result.write(core::ptr::null_mut());
}
}
let dict = unsafe { dict.assume_borrowed_and_cast::<PyDict>(vm) }?;
let dict = writable_dict(unsafe { dict.assume_borrowed() }, "assignment", vm)?;
let key = unsafe { key.assume_borrowed() };
if let Some(value) = dict.inner_getitem_opt(key, vm)? {
@@ -191,7 +217,9 @@ pub unsafe extern "C" fn PyDict_SetDefaultRef(
#[unsafe(no_mangle)]
pub unsafe extern "C" fn PyDict_Size(dict: *mut PyObject) -> isize {
with_vm(|vm| {
let dict = unsafe { dict.assume_borrowed_and_cast::<PyDict>(vm) }?;
let dict = unsafe { dict.assume_borrowed_or_opt() }
.ok_or_else(|| vm.new_system_error("bad argument to internal function"))?;
let dict = any_dict(dict, vm)?;
Ok(dict.__len__())
})
}
@@ -199,7 +227,7 @@ pub unsafe extern "C" fn PyDict_Size(dict: *mut PyObject) -> isize {
#[unsafe(no_mangle)]
pub unsafe extern "C" fn PyDict_Contains(dict: *mut PyObject, key: *mut PyObject) -> c_int {
with_vm(|vm| {
let dict = unsafe { dict.assume_borrowed_and_cast::<PyDict>(vm) }?;
let dict = any_dict(unsafe { dict.assume_borrowed() }, vm)?;
let key = unsafe { key.assume_borrowed() };
Ok(dict.inner_getitem_opt(key, vm)?.is_some())
})
@@ -208,7 +236,9 @@ pub unsafe extern "C" fn PyDict_Contains(dict: *mut PyObject, key: *mut PyObject
#[unsafe(no_mangle)]
pub unsafe extern "C" fn PyDict_Copy(dict: *mut PyObject) -> *mut PyObject {
with_vm(|vm| {
let dict = unsafe { dict.assume_borrowed_and_cast::<PyDict>(vm) }?;
let dict = unsafe { dict.assume_borrowed_or_opt() }
.and_then(|dict| dict.downcast_ref::<PyDict>())
.ok_or_else(|| vm.new_system_error("bad argument to internal function"))?;
Ok(dict.copy().into_ref(&vm.ctx))
})
}
@@ -216,8 +246,12 @@ pub unsafe extern "C" fn PyDict_Copy(dict: *mut PyObject) -> *mut PyObject {
#[unsafe(no_mangle)]
pub unsafe extern "C" fn PyDict_DelItem(dict: *mut PyObject, key: *mut PyObject) -> c_int {
with_vm(|vm| {
let dict = unsafe { dict.assume_borrowed_and_cast::<PyDict>(vm) }?;
let dict = unsafe { dict.assume_borrowed() };
let key = unsafe { key.assume_borrowed() };
if dict.downcast_ref::<PyFrozenDict>().is_some() {
PyFrozenDict::key_hash(key, vm)?;
}
let dict = writable_dict(dict, "deletion", vm)?;
dict.del_item(key, vm)
})
}
@@ -225,8 +259,8 @@ pub unsafe extern "C" fn PyDict_DelItem(dict: *mut PyObject, key: *mut PyObject)
#[unsafe(no_mangle)]
pub unsafe extern "C" fn PyDict_DelItemString(dict: *mut PyObject, key: *const c_char) -> c_int {
with_vm(|vm| {
let dict = unsafe { dict.assume_borrowed_and_cast::<PyDict>(vm) }?;
let key = unsafe { key.try_as_str(vm) }?;
let dict = writable_dict(unsafe { dict.assume_borrowed() }, "deletion", vm)?;
dict.del_item(key, vm)
})
}
@@ -234,7 +268,9 @@ pub unsafe extern "C" fn PyDict_DelItemString(dict: *mut PyObject, key: *const c
#[unsafe(no_mangle)]
pub unsafe extern "C" fn PyDict_Items(dict: *mut PyObject) -> *mut PyObject {
with_vm(|vm| {
let dict = unsafe { dict.assume_borrowed_and_cast::<PyDict>(vm) }?;
let dict = unsafe { dict.assume_borrowed_or_opt() }
.ok_or_else(|| vm.new_system_error("bad argument to internal function"))?;
let dict = any_dict(dict, vm)?;
let items = dict
.items_vec()
.into_iter()
@@ -247,7 +283,9 @@ pub unsafe extern "C" fn PyDict_Items(dict: *mut PyObject) -> *mut PyObject {
#[unsafe(no_mangle)]
pub unsafe extern "C" fn PyDict_Keys(dict: *mut PyObject) -> *mut PyObject {
with_vm(|vm| {
let dict = unsafe { dict.assume_borrowed_and_cast::<PyDict>(vm) }?;
let dict = unsafe { dict.assume_borrowed_or_opt() }
.ok_or_else(|| vm.new_system_error("bad argument to internal function"))?;
let dict = any_dict(dict, vm)?;
Ok(vm.ctx.new_list(dict.keys_vec()))
})
}
@@ -255,7 +293,9 @@ pub unsafe extern "C" fn PyDict_Keys(dict: *mut PyObject) -> *mut PyObject {
#[unsafe(no_mangle)]
pub unsafe extern "C" fn PyDict_Values(dict: *mut PyObject) -> *mut PyObject {
with_vm(|vm| {
let dict = unsafe { dict.assume_borrowed_and_cast::<PyDict>(vm) }?;
let dict = unsafe { dict.assume_borrowed_or_opt() }
.ok_or_else(|| vm.new_system_error("bad argument to internal function"))?;
let dict = any_dict(dict, vm)?;
Ok(vm.ctx.new_list(dict.values_vec()))
})
}
@@ -267,7 +307,7 @@ pub unsafe extern "C" fn PyDict_Merge(
override_: c_int,
) -> c_int {
with_vm(|vm| {
let dict = unsafe { dict.assume_borrowed_and_cast::<PyDict>(vm) }?;
let dict = writable_dict(unsafe { dict.assume_borrowed() }, "assignment", vm)?;
let other = unsafe { other.assume_borrowed() }.to_owned();
if override_ != 0 {
dict.merge_object(other, vm)
@@ -280,7 +320,7 @@ pub unsafe extern "C" fn PyDict_Merge(
#[unsafe(no_mangle)]
pub unsafe extern "C" fn PyDict_Update(dict: *mut PyObject, other: *mut PyObject) -> c_int {
with_vm(|vm| {
let dict = unsafe { dict.assume_borrowed_and_cast::<PyDict>(vm) }?;
let dict = writable_dict(unsafe { dict.assume_borrowed() }, "assignment", vm)?;
let other = unsafe { other.assume_borrowed() }.to_owned();
dict.merge_object(other, vm)
})
@@ -293,7 +333,7 @@ pub unsafe extern "C" fn PyDict_MergeFromSeq2(
override_: c_int,
) -> c_int {
with_vm(|vm| {
let dict = unsafe { dict.assume_borrowed_and_cast::<PyDict>(vm) }?;
let dict = writable_dict(unsafe { dict.assume_borrowed() }, "assignment", vm)?;
let seq2 = unsafe { seq2.assume_borrowed() };
dict.merge_from_seq2(seq2, override_ != 0, vm)
})
@@ -306,8 +346,10 @@ pub unsafe extern "C" fn PyDict_Next(
key: *mut *mut PyObject,
value: *mut *mut PyObject,
) -> c_int {
with_vm(|vm| {
let dict = unsafe { dict.assume_borrowed_and_cast::<PyDict>(vm) }?;
with_vm(|_vm| {
let Some(dict) = PyAnyDict::from_object(unsafe { dict.assume_borrowed() }) else {
return false;
};
let index = unsafe { *pos } as usize;
if let Some((next_pos, k, v)) = dict.next_entry(index) {
@@ -320,9 +362,9 @@ pub unsafe extern "C" fn PyDict_Next(
value.write(v.as_object().as_raw().cast_mut());
}
}
Ok(true)
true
} else {
Ok(false)
false
}
})
}
+2 -2
View File
@@ -2,9 +2,9 @@ use crate::PyObject;
use crate::pystate::with_vm;
use crate::util::FfiPtrExt;
use core::ffi::c_int;
use rustpython_vm::Py;
use rustpython_vm::builtins::PyCode;
use rustpython_vm::frame::FrameObject;
use rustpython_vm::{Py, PyObjectRef};
pub type PyFrameObject = Py<FrameObject>;
pub type PyCodeObject = Py<PyCode>;
@@ -26,7 +26,7 @@ pub unsafe extern "C" fn PyFrame_GetLocals(frame: *mut PyFrameObject) -> *mut Py
#[unsafe(no_mangle)]
pub unsafe extern "C" fn PyFrame_GetGlobals(frame: *mut PyFrameObject) -> *mut PyObject {
with_vm(|_vm| Ok(unsafe { frame.assume_borrowed() }.f_globals()))
with_vm(|_vm| PyObjectRef::from(unsafe { frame.assume_borrowed() }.f_globals()))
}
#[unsafe(no_mangle)]
File diff suppressed because it is too large. Load diff
+2 -2
View File
@@ -2,7 +2,7 @@
*/
use super::{PyAsyncGen, PyCode, PyCoroutine, PyDictRef, PyGenerator, PyIntRef};
use super::{PyAnyDict, PyAsyncGen, PyCode, PyCoroutine, PyGenerator, PyIntRef};
use crate::{
AsObject, Context, Py, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine,
class::PyClassImpl,
@@ -668,7 +668,7 @@ impl Py<FrameObject> {
}
#[pygetset]
pub fn f_globals(&self) -> PyDictRef {
pub fn f_globals(&self) -> PyRef<PyAnyDict> {
self.iframe().globals().to_owned()
}
+17 -15
View File
@@ -2,8 +2,8 @@
mod jit;
use super::{
PyAsyncGen, PyCode, PyCoroutine, PyDictRef, PyGenerator, PyList, PyModule, PyStr, PyStrRef,
PyTuple, PyTupleRef, PyType, object,
PyAnyDict, PyAsyncGen, PyCode, PyCoroutine, PyDictRef, PyGenerator, PyList, PyModule, PyStr,
PyStrRef, PyTuple, PyTupleRef, PyType, object,
};
use crate::common::hash::PyHash;
use crate::common::lock::PyMutex;
@@ -66,7 +66,7 @@ fn format_missing_args(
pub struct PyFunction {
pub(crate) code: PyAtomicRef<PyCode>,
#[pymember(name = "__globals__")]
pub(crate) globals: PyDictRef,
pub(crate) globals: PyRef<PyAnyDict>,
#[pymember(name = "__builtins__")]
pub(crate) builtins: PyObjectRef,
#[pymember(name = "__closure__")]
@@ -179,15 +179,17 @@ impl PyFunction {
#[inline]
pub(crate) fn new(
code: PyRef<PyCode>,
globals: PyDictRef,
globals: PyRef<PyAnyDict>,
vm: &VirtualMachine,
) -> PyResult<Self> {
let name = PyMutex::new(code.obj_name.to_owned());
let module = vm.unwrap_or_none(globals.get_item_opt(identifier!(vm, __name__), vm)?);
let builtins = globals.get_item("__builtins__", vm).unwrap_or_else(|_| {
// If not in globals, inherit from current execution context
crate::frame::current_builtins().unwrap_or_else(|| vm.builtins.dict().into())
});
let module = vm.unwrap_or_none(globals.inner_getitem_opt(identifier!(vm, __name__), vm)?);
let builtins = globals
.inner_getitem_opt(identifier!(vm, __builtins__), vm)?
.unwrap_or_else(|| {
// If not in globals, inherit from current execution context.
crate::frame::current_builtins().unwrap_or_else(|| vm.builtins.dict().into())
});
// If builtins is a module, use its __dict__ instead
let builtins = if let Some(module) = builtins.downcast_ref::<PyModule>() {
module.dict().into()
@@ -621,7 +623,7 @@ impl Py<PyFunction> {
} else if let Some(locals) = locals {
Some(locals)
} else {
Some(ArgMapping::from_dict_exact(self.globals.clone()))
Some(ArgMapping::from_anydict_exact(self.globals.clone()))
};
let use_datastack = !is_gen && !is_coro && !is_async_gen;
let frame = FrameObject::new_ref(
@@ -654,7 +656,7 @@ impl Py<PyFunction> {
} else if let Some(locals) = locals {
crate::frame::FrameLocals::with_locals(locals)
} else {
crate::frame::FrameLocals::with_locals(crate::function::ArgMapping::from_dict_exact(
crate::frame::FrameLocals::with_locals(crate::function::ArgMapping::from_anydict_exact(
self.globals.clone(),
))
};
@@ -805,7 +807,7 @@ impl Py<PyFunction> {
let locals = if code.flags.contains(bytecode::CodeFlags::NEWLOCALS) {
None
} else {
Some(ArgMapping::from_dict_exact(self.globals.clone()))
Some(ArgMapping::from_anydict_exact(self.globals.clone()))
};
let frame = FrameObject::new_ref(
@@ -862,7 +864,7 @@ impl Py<PyFunction> {
let locals = if code.flags.contains(bytecode::CodeFlags::NEWLOCALS) {
None
} else {
Some(ArgMapping::from_dict_exact(self.globals.clone()))
Some(ArgMapping::from_anydict_exact(self.globals.clone()))
};
// Heap-backed: the frame outlives the call that made it.
@@ -897,7 +899,7 @@ impl Py<PyFunction> {
let locals = if code.flags.contains(bytecode::CodeFlags::NEWLOCALS) {
crate::frame::FrameLocals::lazy()
} else {
crate::frame::FrameLocals::with_locals(ArgMapping::from_dict_exact(
crate::frame::FrameLocals::with_locals(ArgMapping::from_anydict_exact(
self.globals.clone(),
))
};
@@ -1318,7 +1320,7 @@ impl Constructor for PyFunction {
None
};
let mut func = Self::new(args.code.clone(), args.globals.clone(), vm)?;
let mut func = Self::new(args.code.clone(), args.globals.clone().into(), vm)?;
// Set function name if provided
if let Some(name) = args.name.into_option() {
*func.name.lock() = name;
+6
View File
@@ -68,6 +68,12 @@ impl Constructor for PyMappingProxy {
}
impl PyMappingProxy {
pub(crate) fn from_any_dict(dict: PyRef<super::PyAnyDict>) -> Self {
Self {
mapping: MappingProxyInner::Mapping(ArgMapping::new(dict.into())),
}
}
pub fn from_object(mapping: PyObjectRef, vm: &VirtualMachine) -> PyResult<Self> {
if mapping.mapping_unchecked().check()
&& !mapping.downcastable::<PyList>()
+1 -1
View File
@@ -20,7 +20,7 @@ pub use complex::PyComplex;
pub(crate) mod coroutine;
pub use coroutine::PyCoroutine;
pub(crate) mod dict;
pub use dict::{PyDict, PyDictRef};
pub use dict::{PyAnyDict, PyDict, PyDictRef, PyFrozenDict};
pub(crate) mod enumerate;
pub use enumerate::PyEnumerate;
pub(crate) mod filter;
+27 -18
View File
@@ -1,6 +1,6 @@
use super::{
PyClassMethod, PyDict, PyDictRef, PyList, PyStaticMethod, PyStr, PyStrInterned, PyStrRef,
PyTupleRef, PyUtf8StrRef, PyWeak, mappingproxy::PyMappingProxy, object, union_,
PyAnyDict, PyClassMethod, PyDict, PyDictRef, PyList, PyStaticMethod, PyStr, PyStrInterned,
PyStrRef, PyTupleRef, PyUtf8StrRef, PyWeak, mappingproxy::PyMappingProxy, object, union_,
};
use crate::{
AsObject, Context, Py, PyAtomicRef, PyObject, PyObjectRef, PyPayload, PyRef, PyResult,
@@ -1798,25 +1798,15 @@ impl Constructor for PyType {
}));
}
let (name, bases, dict, kwargs): (PyStrRef, PyTupleRef, PyDictRef, KwArgs) =
let (name, bases, dict, kwargs): (PyStrRef, PyTupleRef, PyObjectRef, KwArgs) =
args.clone().bind_for(vm, Self::NAME)?;
// A mapping that is not an exact dict (e.g. OrderedDict) keeps its
// own iteration order; copy via the mapping protocol so that order
// lands in the type dict.
let dict = if args.args[2].class().is(vm.ctx.types.dict_type) {
dict
} else {
let copied = vm.ctx.new_dict();
copied.merge_object(args.args[2].clone(), vm)?;
copied
let Some(dict) = PyAnyDict::from_object(&dict) else {
return Err(vm.new_type_error(format!(
"type.__new__() argument 3 must be dict or frozendict, not {}",
dict.class().fully_qualified_name(vm)?
)));
};
if name.as_bytes().contains(&0) {
return Err(vm.new_value_error("type name must not contain null characters"));
}
let name = name.try_into_utf8(vm)?;
let (metatype, base, bases, base_is_type) = if bases.as_slice().is_empty() {
let base = vm.ctx.types.object_type.to_owned();
let bases = PyTuple::new_ref_typed(vec![base.clone()], &vm.ctx);
@@ -1856,6 +1846,25 @@ impl Constructor for PyType {
(metatype, base.to_owned(), bases, base_is_type)
};
// Copy only after resolving the metaclass: its __new__ receives the
// original namespace. A subclass with a custom iterator (e.g.
// OrderedDict) keeps its own mapping order in the mutable type dict.
let dict = if let Some(dict) = dict.as_object().downcast_ref_if_exact::<PyDict>(vm) {
dict.to_owned()
} else {
let copied = vm.ctx.new_dict();
// Like dict.copy(), an empty table bypasses subclass hooks.
if !dict.is_empty() {
copied.merge_object(dict.to_owned().into(), vm)?;
}
copied
};
if name.as_bytes().contains(&0) {
return Err(vm.new_value_error("type name must not contain null characters"));
}
let name = name.try_into_utf8(vm)?;
let qualname = dict
.get_item_opt(identifier!(vm, __qualname__), vm)?
.map(|obj| downcast_qualname(obj, vm))
+61 -44
View File
@@ -6,10 +6,10 @@ use crate::{
AsObject, Py, PyExact, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, PyStackRef,
TryFromObject, VirtualMachine,
builtins::{
PyBaseException, PyBaseExceptionRef, PyBaseObject, PyBytes, PyCode, PyCoroutine, PyDict,
PyDictRef, PyFloat, PyFrozenSet, PyGenerator, PyInt, PyInterpolation, PyList, PyModule,
PyProperty, PySet, PySlice, PyStr, PyStrInterned, PyTemplate, PyTraceback, PyType,
PyUtf8Str,
PyAnyDict, PyBaseException, PyBaseExceptionRef, PyBaseObject, PyBytes, PyCode, PyCoroutine,
PyDict, PyDictRef, PyFloat, PyFrozenSet, PyGenerator, PyInt, PyInterpolation, PyList,
PyModule, PyProperty, PySet, PySlice, PyStr, PyStrInterned, PyTemplate, PyTraceback,
PyType, PyUtf8Str,
builtin_func::PyNativeFunction,
descriptor::{PyMemberDescriptor, PyMethodDescriptor},
frame::stack_analysis,
@@ -108,7 +108,7 @@ pub fn current_thread_frame_materialize(vm: &VirtualMachine) -> Option<FrameObje
/// Read the globals dict from the topmost frame on this thread's chain.
/// Returns `None` if the chain is empty.
#[must_use]
pub fn current_globals() -> Option<PyDictRef> {
pub fn current_globals() -> Option<PyRef<PyAnyDict>> {
let ptr = crate::vm::thread::get_current_frame();
if ptr.is_null() {
return None;
@@ -923,7 +923,7 @@ pub struct InterpreterFrame {
// Borrowed pointers — owned by FrameObject or by PyFunction on caller's stack.
pub(crate) code: *const Py<PyCode>,
pub(crate) func_obj: *const PyObject, // nullable
pub(crate) globals: *const Py<PyDict>,
pub(crate) globals: *const PyAnyDict,
pub(crate) builtins: *const PyObject,
/// Unified storage for local variables and evaluation stack.
@@ -983,7 +983,7 @@ impl InterpreterFrame {
#[inline(always)]
pub(crate) unsafe fn new(
code: &Py<PyCode>,
globals: &Py<PyDict>,
globals: &PyAnyDict,
builtins: &PyObject,
func_obj: Option<&PyObject>,
localsplus: LocalsPlus,
@@ -1027,7 +1027,7 @@ impl InterpreterFrame {
Some(obj) => obj as *const PyObject,
None => core::ptr::null(),
},
globals: globals as *const Py<PyDict>,
globals: globals as *const PyAnyDict,
builtins: builtins as *const PyObject,
localsplus,
locals,
@@ -1060,7 +1060,7 @@ impl InterpreterFrame {
#[inline(always)]
pub(crate) unsafe fn new_on_datastack<'a>(
code: &Py<PyCode>,
globals: &Py<PyDict>,
globals: &PyAnyDict,
builtins: &PyObject,
func_obj: Option<&PyObject>,
locals: FrameLocals,
@@ -1266,7 +1266,7 @@ impl InterpreterFrame {
// Create a full FrameObject with its own InterpreterFrame copy.
// The FrameObject owns references to the same objects (code, globals, etc).
let code: PyRef<PyCode> = self.code().to_owned();
let globals: PyDictRef = self.globals().to_owned();
let globals = self.globals().to_owned();
let builtins: PyObjectRef = self.builtins().to_owned();
let func_obj: Option<PyObjectRef> = self.func_obj().map(|o| o.to_owned());
@@ -1362,7 +1362,7 @@ impl InterpreterFrame {
#[cold]
fn materialize_slow_chain(&self, vm: &VirtualMachine) -> FrameObjectRef {
let code: PyRef<PyCode> = self.code().to_owned();
let globals: PyDictRef = self.globals().to_owned();
let globals = self.globals().to_owned();
let builtins: PyObjectRef = self.builtins().to_owned();
let func_obj: Option<PyObjectRef> = self.func_obj().map(|o| o.to_owned());
@@ -1419,7 +1419,7 @@ impl InterpreterFrame {
/// Borrowed globals dict.
#[inline(always)]
pub fn globals(&self) -> &Py<PyDict> {
pub fn globals(&self) -> &PyAnyDict {
// SAFETY: established by `new` / `init_iframe_ptrs`.
unsafe { &*self.globals }
}
@@ -1534,7 +1534,7 @@ pub struct FrameObject {
// raw pointers. Wrapped in Option so Traverse::clear can release them,
// allowing GC cycle collection to reclaim referenced objects.
pub(crate) owned_code: Option<PyRef<PyCode>>,
pub(crate) owned_globals: Option<PyDictRef>,
pub(crate) owned_globals: Option<PyRef<PyAnyDict>>,
pub(crate) owned_builtins: Option<PyObjectRef>,
pub(crate) owned_func_obj: Option<PyObjectRef>,
@@ -1817,7 +1817,7 @@ impl FrameObject {
let locals = match scope.locals {
Some(locals) => FrameLocals::with_locals(locals),
None if code.flags.contains(bytecode::CodeFlags::NEWLOCALS) => FrameLocals::lazy(),
None => FrameLocals::with_locals(ArgMapping::from_dict_exact(scope.globals.clone())),
None => FrameLocals::with_locals(ArgMapping::from_anydict_exact(scope.globals.clone())),
};
// Pointers are initially set from owned fields' references but will be
@@ -1853,7 +1853,7 @@ impl FrameObject {
fn init_iframe_ptrs(self_: &Py<Self>) {
let iframe = unsafe { self_.iframe_mut() };
iframe.code = &**self_.owned_code.as_ref().unwrap() as *const Py<PyCode>;
iframe.globals = &**self_.owned_globals.as_ref().unwrap() as *const Py<PyDict>;
iframe.globals = &**self_.owned_globals.as_ref().unwrap() as *const PyAnyDict;
iframe.builtins = &**self_.owned_builtins.as_ref().unwrap() as *const PyObject;
iframe.func_obj = match &self_.owned_func_obj {
Some(obj) => &**obj as *const PyObject,
@@ -2497,7 +2497,7 @@ impl Py<FrameObject> {
},
)
};
let builtins_dict = if globals.class().is(vm.ctx.types.dict_type) {
let builtins_dict = if globals.as_object().class().is(vm.ctx.types.dict_type) {
builtins
.downcast_ref_if_exact::<PyDict>(vm)
// SAFETY: downcast_ref_if_exact already verified exact type
@@ -2739,7 +2739,7 @@ fn exec_iframe<'a>(
},
)
};
let builtins_dict = if globals.class().is(vm.ctx.types.dict_type) {
let builtins_dict = if globals.as_object().class().is(vm.ctx.types.dict_type) {
builtins
.downcast_ref_if_exact::<PyDict>(vm)
.map(|d| unsafe { PyExact::ref_unchecked(d) })
@@ -2931,7 +2931,7 @@ pub(crate) struct ExecutingFrame<'a> {
code: &'a Py<PyCode>,
localsplus: &'a mut LocalsPlus,
locals: &'a FrameLocals,
globals: &'a Py<PyDict>,
globals: &'a PyAnyDict,
builtins: &'a PyObject,
/// Cached downcast of builtins to PyDict for fast LOAD_GLOBAL.
/// Only set when both globals and builtins are exact dict types (not
@@ -4651,15 +4651,10 @@ impl ExecutingFrame<'_> {
}
Instruction::DeleteName { namei: idx } => {
let name = self.code.names[idx.get(arg) as usize];
let res = self.locals.mapping(vm).ass_subscript(name, None, vm);
match res {
Ok(()) => {}
Err(e) if e.fast_isinstance(vm.ctx.exceptions.key_error) => {
return Err(name_error(name, vm));
}
Err(e) => return Err(e),
}
self.locals
.mapping(vm)
.ass_subscript(name, None, vm)
.map_err(|_| name_error(name, vm))?;
Ok(None)
}
Instruction::DeleteSubscr => self.execute_delete_subscript(vm),
@@ -5254,7 +5249,7 @@ impl ExecutingFrame<'_> {
match result {
Ok(x) => self.push_value(x),
Err(e) if e.fast_isinstance(vm.ctx.exceptions.key_error) => {
self.push_value(self.load_global_or_builtin(name, vm)?);
self.push_value(self.load_name_global_or_builtin(name, vm)?);
}
Err(e) => return Err(e),
}
@@ -8043,9 +8038,9 @@ impl ExecutingFrame<'_> {
let cached_version = self.code.instructions.read_cache_u16(cache_base + 1);
let cached_index = self.code.instructions.read_cache_u16(cache_base + 3);
if cached_version != 0
&& let Some(x) = self
.globals
.get_item_by_index_and_keys_version(cached_version, cached_index)
&& let Some(globals) = self.globals.as_mutable()
&& let Some(x) =
globals.get_item_by_index_and_keys_version(cached_version, cached_index)
{
self.push_value(x);
if (oparg & 1) != 0 {
@@ -8069,7 +8064,8 @@ impl ExecutingFrame<'_> {
let cached_index = self.code.instructions.read_cache_u16(cache_base + 3);
if cached_globals_ver != 0
&& cached_builtins_ver != 0
&& let Ok(current_globals_ver) = u16::try_from(self.globals.keys_version())
&& let Some(globals) = self.globals.as_mutable()
&& let Ok(current_globals_ver) = u16::try_from(globals.keys_version())
&& cached_globals_ver == current_globals_ver
&& let Some(builtins_dict) = self.builtins.downcast_ref_if_exact::<PyDict>(vm)
&& let Some(x) = builtins_dict
@@ -8541,7 +8537,12 @@ impl ExecutingFrame<'_> {
if let Some(builtins_dict) = self.builtins_dict {
// Fast path: both globals and builtins are exact dicts
// SAFETY: builtins_dict is only set when globals is also exact dict
let globals_exact = unsafe { PyExact::ref_unchecked(self.globals) };
let globals = self
.globals
.as_object()
.downcast_ref::<PyDict>()
.expect("exact dict globals");
let globals_exact = unsafe { PyExact::ref_unchecked(globals) };
globals_exact
.get_chain_exact(builtins_dict, name, vm)?
.ok_or_else(|| {
@@ -8552,16 +8553,29 @@ impl ExecutingFrame<'_> {
if let Some(value) = self.globals.get_item_opt(name, vm)? {
return Ok(value);
}
self.builtins.get_item(name, vm).map_err(|e| {
if e.fast_isinstance(vm.ctx.exceptions.key_error) {
vm.new_name_error(format!("name '{name}' is not defined"), name.to_owned())
} else {
e
}
})
self.load_builtin(name, vm)
}
}
fn load_name_global_or_builtin(&self, name: &Py<PyStr>, vm: &VirtualMachine) -> PyResult {
// LOAD_NAME reads the stored global entries even for dict subclasses;
// LOAD_GLOBAL instead honors their __getitem__ implementation.
if let Some(value) = self.globals.inner_getitem_opt(name, vm)? {
return Ok(value);
}
self.load_builtin(name, vm)
}
fn load_builtin(&self, name: &Py<PyStr>, vm: &VirtualMachine) -> PyResult {
self.builtins.get_item(name, vm).map_err(|e| {
if e.fast_isinstance(vm.ctx.exceptions.key_error) {
vm.new_name_error(format!("name '{name}' is not defined"), name.to_owned())
} else {
e
}
})
}
#[cfg_attr(feature = "flame-it", flame("FrameObject"))]
fn import(&mut self, vm: &VirtualMachine, module_name: Option<&Py<PyStr>>) -> PyResult<()> {
let module_name = module_name.unwrap_or(vm.ctx.empty_str);
@@ -11786,7 +11800,7 @@ impl ExecutingFrame<'_> {
let locals = if code.flags.contains(bytecode::CodeFlags::NEWLOCALS) {
FrameLocals::lazy()
} else {
FrameLocals::with_locals(crate::function::ArgMapping::from_dict_exact(
FrameLocals::with_locals(crate::function::ArgMapping::from_anydict_exact(
func.globals.clone(),
))
};
@@ -11852,7 +11866,7 @@ impl ExecutingFrame<'_> {
let locals = if code.flags.contains(bytecode::CodeFlags::NEWLOCALS) {
FrameLocals::lazy()
} else {
FrameLocals::with_locals(crate::function::ArgMapping::from_dict_exact(
FrameLocals::with_locals(crate::function::ArgMapping::from_anydict_exact(
func.globals.clone(),
))
};
@@ -11938,8 +11952,11 @@ impl ExecutingFrame<'_> {
) {
return;
}
let Some(globals) = self.globals.as_mutable() else {
return;
};
let name = self.code.names[(oparg >> 1) as usize];
let Ok(globals_version @ 1..) = u16::try_from(self.globals.assign_keys_version(vm)) else {
let Ok(globals_version @ 1..) = u16::try_from(globals.assign_keys_version(vm)) else {
unsafe {
self.code.instructions.write_adaptive_counter(
cache_base,
@@ -11951,7 +11968,7 @@ impl ExecutingFrame<'_> {
return;
};
if let Ok(Some(globals_hint)) = self.globals.hint_for_key(name, vm) {
if let Ok(Some(globals_hint)) = globals.hint_for_key(name, vm) {
unsafe {
self.code
.instructions
+8 -2
View File
@@ -1,7 +1,7 @@
use super::IntoFuncArgs;
use crate::{
AsObject, PyObject, PyObjectRef, PyPayload, PyResult, TryFromObject, VirtualMachine,
builtins::{PyDictRef, iter::PySequenceIterator},
AsObject, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, TryFromObject, VirtualMachine,
builtins::{PyAnyDict, PyDictRef, iter::PySequenceIterator},
convert::ToPyObject,
object::{Traverse, TraverseFn},
protocol::{PyIter, PyIterIter, PyMapping},
@@ -155,6 +155,12 @@ impl ArgMapping {
Self { obj: dict.into() }
}
#[inline(always)]
#[must_use]
pub fn from_anydict_exact(dict: PyRef<PyAnyDict>) -> Self {
Self { obj: dict.into() }
}
#[inline(always)]
#[must_use]
pub fn obj(&self) -> &PyObject {
+6
View File
@@ -2666,6 +2666,7 @@ mod ref_target_sealed {
impl<T: super::PyPayload> Sealed for T {}
impl Sealed for super::PyObject {}
impl Sealed for crate::builtins::set::PyAnySet {}
impl Sealed for crate::builtins::PyAnyDict {}
impl<T: super::PyTypedRefTarget> Sealed for super::PyTuple<super::PyRef<T>> {}
}
@@ -2709,6 +2710,11 @@ pub trait PyTypedRefTarget: PyRefTarget {}
impl<T: PyRefTarget<Target = Py<T>>> PyTypedRefTarget for T {}
impl PyTypedRefTarget for crate::builtins::set::PyAnySet {}
impl PyTypedRefTarget for crate::builtins::PyAnyDict {}
impl PyRefTarget for crate::builtins::PyAnyDict {
type Target = Self;
}
impl PyRefTarget for crate::builtins::set::PyAnySet {
type Target = Self;
+5
View File
@@ -78,6 +78,11 @@ impl<'a> PyCallable<'a> {
pub fn invoke(&self, args: impl IntoFuncArgs, vm: &VirtualMachine) -> PyResult {
let args = args.into_args(vm);
// _PyVectorcall_Call
if self.vectorcall.is_some() && args.kwargs.is_empty() {
let nargs = args.args.len();
return self.invoke_vectorcall(args.args, nargs, None, vm);
}
if !vm.use_tracing.get() {
return (self.call)(self.obj, args, vm);
}
+3 -3
View File
@@ -184,7 +184,7 @@ impl PyMapping<'_> {
pub fn keys(self, vm: &VirtualMachine) -> PyResult {
if let Some(dict) = self.obj.downcast_ref_if_exact::<PyDict>(vm) {
PyDictKeys::new(dict.to_owned()).to_pyresult(vm)
PyDictKeys::new(dict.to_owned().into()).to_pyresult(vm)
} else {
self.method_output_as_list(identifier!(vm, keys), vm)
}
@@ -192,7 +192,7 @@ impl PyMapping<'_> {
pub fn values(self, vm: &VirtualMachine) -> PyResult {
if let Some(dict) = self.obj.downcast_ref_if_exact::<PyDict>(vm) {
PyDictValues::new(dict.to_owned()).to_pyresult(vm)
PyDictValues::new(dict.to_owned().into()).to_pyresult(vm)
} else {
self.method_output_as_list(identifier!(vm, values), vm)
}
@@ -200,7 +200,7 @@ impl PyMapping<'_> {
pub fn items(self, vm: &VirtualMachine) -> PyResult {
if let Some(dict) = self.obj.downcast_ref_if_exact::<PyDict>(vm) {
PyDictItems::new(dict.to_owned()).to_pyresult(vm)
PyDictItems::new(dict.to_owned().into()).to_pyresult(vm)
} else {
self.method_output_as_list(identifier!(vm, items), vm)
}
+11 -4
View File
@@ -1,10 +1,14 @@
use crate::{VirtualMachine, builtins::PyDictRef, function::ArgMapping};
use crate::{
PyRef, VirtualMachine,
builtins::{PyAnyDict, PyDictRef},
function::ArgMapping,
};
use alloc::fmt;
#[derive(Clone)]
pub struct Scope {
pub locals: Option<ArgMapping>,
pub globals: PyDictRef,
pub globals: PyRef<PyAnyDict>,
}
impl fmt::Debug for Scope {
@@ -17,8 +21,11 @@ impl fmt::Debug for Scope {
impl Scope {
#[inline]
#[must_use]
pub const fn new(locals: Option<ArgMapping>, globals: PyDictRef) -> Self {
Self { locals, globals }
pub fn new(locals: Option<ArgMapping>, globals: impl Into<PyRef<PyAnyDict>>) -> Self {
Self {
locals,
globals: globals.into(),
}
}
pub fn with_builtins(
+23 -15
View File
@@ -1125,11 +1125,16 @@ fn expandtabs(input: &str, tab_size: usize) -> String {
expanded
}
type CodeNamespaces = (PyRef<PyCode>, Option<PyRef<PyDict>>, Option<PyRef<PyDict>>);
type CodeNamespaces = (
PyRef<PyCode>,
Option<PyRef<crate::builtins::PyAnyDict>>,
Option<PyRef<crate::builtins::PyAnyDict>>,
);
fn code_or_function(obj: &PyObject, vm: &VirtualMachine) -> PyResult<CodeNamespaces> {
if let Ok(func) = obj.to_owned().downcast::<PyFunction>() {
let builtins = func.builtins.clone().downcast::<PyDict>().ok();
let builtins =
crate::builtins::PyAnyDict::from_object(&func.builtins).map(ToOwned::to_owned);
return Ok((
(*func.code).to_owned(),
Some(func.globals.clone()),
@@ -1142,19 +1147,22 @@ fn code_or_function(obj: &PyObject, vm: &VirtualMachine) -> PyResult<CodeNamespa
Err(vm.new_type_error("argument must be a code object or a function"))
}
fn optional_dict(
fn optional_dict<T: From<PyRef<PyDict>>>(
override_ns: OptionalArg<PyObjectRef>,
default: Option<PyRef<PyDict>>,
default: Option<T>,
name: &str,
vm: &VirtualMachine,
) -> PyResult<Option<PyRef<PyDict>>> {
) -> PyResult<Option<T>> {
match override_ns.into_option() {
Some(obj) => obj.downcast::<PyDict>().map(Some).map_err(|obj| {
vm.new_type_error(format!(
"expected a dict for \"{name}\", got {}",
obj.class().name()
))
}),
Some(obj) => obj
.downcast::<PyDict>()
.map(|dict| Some(dict.into()))
.map_err(|obj| {
vm.new_type_error(format!(
"expected a dict for \"{name}\", got {}",
obj.class().name()
))
}),
None => Ok(default),
}
}
@@ -1280,8 +1288,8 @@ fn set_unbound_var_counts(
counts: &mut VarCounts,
globalnames: Option<PyObjectRef>,
attrnames: Option<PyObjectRef>,
globalsns: Option<&Py<PyDict>>,
builtinsns: Option<&Py<PyDict>>,
globalsns: Option<&crate::builtins::PyAnyDict>,
builtinsns: Option<&crate::builtins::PyAnyDict>,
vm: &VirtualMachine,
) -> PyResult<()> {
let globalnames = optional_set(globalnames, "globalnames", vm)?;
@@ -1317,8 +1325,8 @@ fn identify_unbound_names(
code: &Py<PyCode>,
globalnames: Option<PyRef<PySet>>,
attrnames: Option<PyRef<PySet>>,
globalsns: Option<&Py<PyDict>>,
builtinsns: Option<&Py<PyDict>>,
globalsns: Option<&crate::builtins::PyAnyDict>,
builtinsns: Option<&crate::builtins::PyAnyDict>,
vm: &VirtualMachine,
) -> PyResult<(UnboundCounts, i32)> {
let mut seen_globals = HashSet::new();
+48 -14
View File
@@ -11,7 +11,7 @@ mod builtins {
AsObject, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, TryFromObject,
VirtualMachine,
builtins::{
PyByteArray, PyBytes, PyDictRef, PyStr, PyStrRef, PyTuple, PyTupleRef, PyType,
PyAnyDict, PyByteArray, PyBytes, PyStr, PyStrRef, PyTuple, PyTupleRef, PyType,
PyUtf8StrRef,
enumerate::PyReverseSequenceIterator,
function::{PyCell, PyCellRef, PyFunction},
@@ -425,6 +425,26 @@ mod builtins {
locals: Option<ArgMapping>,
}
/// `_PyEval_EnsureBuiltins`
fn ensure_builtins(globals: &PyAnyDict, vm: &VirtualMachine) -> PyResult<()> {
if globals
.inner_getitem_opt(identifier!(vm, __builtins__), vm)?
.is_some()
{
return Ok(());
}
// PyEval_GetBuiltins
let builtins =
crate::frame::current_builtins().unwrap_or_else(|| vm.builtins.dict().into());
match globals.as_mutable() {
Some(dict) => dict.inner_setitem(identifier!(vm, __builtins__), builtins, vm),
None => globals
.as_object()
.set_item(identifier!(vm, __builtins__), builtins, vm)
.map_err(|_| vm.new_type_error("cannot assign __builtins__ to frozendict globals")),
}
}
impl ScopeArgs {
fn make_scope(
self,
@@ -436,21 +456,23 @@ mod builtins {
vm: &VirtualMachine,
func_name: &'static str,
) -> PyResult<()> {
if !globals.fast_isinstance(vm.ctx.types.dict_type) {
if !globals.fast_isinstance(vm.ctx.types.dict_type)
&& !globals.fast_isinstance(vm.ctx.types.frozendict_type)
{
return Err(match func_name {
"eval" => {
let is_mapping = globals.mapping_unchecked().check();
vm.new_type_error(if is_mapping {
"globals must be a real dict; try eval(expr, {}, mapping)"
"globals must be a real dict or a frozendict; try eval(expr, {}, mapping)"
} else {
"globals must be a dict"
"globals must be a dict or a frozendict"
})
}
"exec" => vm.new_type_error(format!(
"exec() globals must be a dict, not {}",
"exec() globals must be a dict or a frozendict, not {}",
globals.class().name()
)),
_ => vm.new_type_error("globals must be a dict"),
_ => vm.new_type_error("globals must be a dict or a frozendict"),
});
}
Ok(())
@@ -460,15 +482,11 @@ mod builtins {
Some(globals) => {
validate_globals_dict(&globals, vm, func_name)?;
let globals = PyDictRef::try_from_object(vm, globals)?;
if !globals.contains_key(identifier!(vm, __builtins__), vm) {
let builtins_dict = vm.builtins.dict().into();
globals.set_item(identifier!(vm, __builtins__), builtins_dict, vm)?;
}
let globals = PyRef::<PyAnyDict>::try_from_object(vm, globals)?;
(
globals.clone(),
self.locals
.unwrap_or_else(|| ArgMapping::from_dict_exact(globals.clone())),
.unwrap_or_else(|| ArgMapping::from_anydict_exact(globals.clone())),
)
}
None => (
@@ -481,7 +499,9 @@ mod builtins {
),
};
let scope = crate::scope::Scope::with_builtins(Some(locals), globals, vm);
ensure_builtins(&globals, vm)?;
let scope = crate::scope::Scope::new(Some(locals), globals);
Ok(scope)
}
}
@@ -650,6 +670,7 @@ mod builtins {
func: &str,
closure: Option<PyRef<PyTuple<PyCellRef>>>,
) -> PyResult {
let from_string = matches!(&source, Either::B(_));
// Determine code object:
let code_obj = match source {
#[cfg(feature = "rustpython-compiler")]
@@ -675,6 +696,18 @@ mod builtins {
)));
}
// run_eval_code_obj: set globals['__builtins__'] if it doesn't exist
if from_string
&& scope
.globals
.inner_getitem_opt(identifier!(vm, __builtins__), vm)?
.is_none()
{
scope
.globals
.set_item(identifier!(vm, __builtins__), vm.builtins.dict().into(), vm)?;
}
// Run the code:
vm.run_code_obj_with_closure(code_obj, scope, closure)
}
@@ -721,7 +754,7 @@ mod builtins {
}
#[pyfunction]
fn globals(vm: &VirtualMachine) -> PyDictRef {
fn globals(vm: &VirtualMachine) -> PyRef<PyAnyDict> {
vm.current_globals()
}
@@ -1490,6 +1523,7 @@ pub fn init_module(vm: &VirtualMachine, module: &Py<PyModule>) {
"dict" => ctx.types.dict_type.to_owned(),
"enumerate" => ctx.types.enumerate_type.to_owned(),
"float" => ctx.types.float_type.to_owned(),
"frozendict" => ctx.types.frozendict_type.to_owned(),
"frozenset" => ctx.types.frozenset_type.to_owned(),
"filter" => ctx.types.filter_type.to_owned(),
"int" => ctx.types.int_type.to_owned(),
+2
View File
@@ -40,6 +40,7 @@ pub struct TypeZoo {
pub float_type: &'static Py<PyType>,
pub frame_type: &'static Py<PyType>,
pub frame_locals_proxy_type: &'static Py<PyType>,
pub frozendict_type: &'static Py<PyType>,
pub frozenset_type: &'static Py<PyType>,
pub generator_type: &'static Py<PyType>,
pub int_type: &'static Py<PyType>,
@@ -139,6 +140,7 @@ impl TypeZoo {
dict_type: dict::PyDict::init_builtin_type(),
enumerate_type: enumerate::PyEnumerate::init_builtin_type(),
float_type: float::PyFloat::init_builtin_type(),
frozendict_type: dict::PyFrozenDict::init_builtin_type(),
frozenset_type: set::PyFrozenSet::init_builtin_type(),
filter_type: filter::PyFilter::init_builtin_type(),
list_type: list::PyList::init_builtin_type(),
+8 -3
View File
@@ -340,7 +340,7 @@ impl SharedValue {
// Stateless functions have no globals, so `__main__` is used,
// just like for builtins such as exec().
let globals = vm.main_namespace()?;
Ok(PyFunction::new(code, globals, vm)?.into_pyobject(vm))
Ok(PyFunction::new(code, globals.into(), vm)?.into_pyobject(vm))
}
Self::Pickled(data) => pickle_loads(&data, vm),
}
@@ -763,7 +763,12 @@ pub(crate) fn walk_instructions(
/// `_PyFunction_VerifyStateless`.
fn verify_stateless_function(func: &Py<PyFunction>, vm: &VirtualMachine) -> PyResult<()> {
// `__globals__` is a dict by construction, so only the builtins are checked.
let globals = func.globals.as_mutable().ok_or_else(|| {
vm.new_type_error(format!(
"unsupported globals {}",
render_repr(func.globals.as_object(), vm)
))
})?;
let builtins = func.builtins.downcast_ref::<PyDict>().ok_or_else(|| {
vm.new_type_error(format!(
"unsupported builtins {}",
@@ -786,7 +791,7 @@ fn verify_stateless_function(func: &Py<PyFunction>, vm: &VirtualMachine) -> PyRe
{
return Err(vm.new_value_error("closures not supported"));
}
verify_stateless(&func.code, Some((&func.globals, builtins)), vm)
verify_stateless(&func.code, Some((globals, builtins)), vm)
}
/// `verify_script`: a script takes no arguments and returns only None.
+3 -3
View File
@@ -24,7 +24,7 @@ mod vm_ops;
use crate::{
AsObject, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult,
builtins::{
self, PyBaseExceptionRef, PyBaseObject, PyDict, PyDictRef, PyFrozenSet, PyInt, PyList,
self, PyAnyDict, PyBaseExceptionRef, PyBaseObject, PyDict, PyFrozenSet, PyInt, PyList,
PyModule, PySet, PyStr, PyStrInterned, PyStrRef, PyTypeRef, PyUtf8Str, PyUtf8StrInterned,
PyWeak,
code::PyCode,
@@ -3157,7 +3157,7 @@ impl VirtualMachine {
.locals(self)
}
pub fn current_globals(&self) -> PyDictRef {
pub fn current_globals(&self) -> PyRef<PyAnyDict> {
let ptr = crate::vm::thread::get_current_frame();
if !ptr.is_null() {
return unsafe { (*ptr).globals().to_owned() };
@@ -3270,7 +3270,7 @@ impl VirtualMachine {
// Locals fallback: use the heavy frame if available, otherwise
// use globals as locals (light frame locals are on the data stack).
let locals_mapping = self.current_frame().map_or_else(
|| ArgMapping::from_dict_exact(globals.clone()),
|| ArgMapping::from_anydict_exact(globals.clone()),
|f| f.iframe().locals.clone_mapping(self),
);
(Some(locals_mapping), Some(globals))
+4 -3
View File
@@ -83,15 +83,16 @@ impl VirtualMachine {
}
pub fn new_scope_with_main(&self) -> PyResult<Scope> {
let scope = self.new_scope_with_builtins();
let main_module = self.new_module("__main__", scope.globals.clone(), None);
let globals = self.ctx.new_dict();
let scope = Scope::with_builtins(None, globals.clone(), self);
let main_module = self.new_module("__main__", globals.clone(), None);
self.sys_module.get_attr("modules", self)?.set_item(
"__main__",
main_module.into(),
self,
)?;
self.set_main_builtin_importer(&scope.globals)?;
self.set_main_builtin_importer(&globals)?;
Ok(scope)
}
+7 -6
View File
@@ -762,12 +762,12 @@ fn setup_context(
d.downcast::<crate::builtins::PyDict>()
.map_err(|_| vm.new_type_error("sys.__dict__ is not a dictionary"))
})?;
(globals, vm.ctx.intern_str("<sys>"), 0)
(globals.into(), vm.ctx.intern_str("<sys>"), 0)
};
let registry = match globals.get_item("__warningregistry__", vm) {
Ok(r) => r,
Err(_) => {
let registry = match globals.inner_getitem_opt("__warningregistry__", vm)? {
Some(r) => r,
None => {
let r = vm.ctx.new_dict();
globals.set_item("__warningregistry__", r.clone().into(), vm)?;
r.into()
@@ -776,7 +776,8 @@ fn setup_context(
// Setup module.
let module = globals
.get_item("__name__", vm)
.unwrap_or_else(|_| vm.new_pyobj("<string>"));
.inner_getitem_opt("__name__", vm)?
.filter(|module| vm.is_none(module) || module.downcastable::<PyStr>())
.unwrap_or_else(|| vm.new_pyobj("<string>"));
Ok((filename.to_owned(), lineno, Some(module), registry))
}
+294
View File
@@ -873,3 +873,297 @@ with assert_raises(KeyError):
# entirely when popping from an empty dict).
{1: 1}.pop(CountingHash())
assert CountingHash.calls == 1, CountingHash.calls
# frozendict as a namespace, beyond what CPython's own tests cover.
import builtins
import gc
import sys
import types
import unittest
class FrozenDictDescriptorTests(unittest.TestCase):
def test_unrelated_descriptors_cannot_mutate_or_access(self):
frozen = frozendict(protected=23)
for method, args in (
(dict.__init__, ({"replacement": 8},)),
(dict.__setitem__, ("protected", 99)),
(dict.__delitem__, ("protected",)),
(dict.update, ({"replacement": 8},)),
(dict.clear, ()),
(dict.get, ("protected",)),
):
with self.subTest(method=method), self.assertRaises(TypeError):
method(frozen, *args)
with self.assertRaises(TypeError):
frozendict.get({"protected": 23}, "protected")
self.assertEqual(frozen, {"protected": 23})
for name in (
"clear",
"update",
"pop",
"popitem",
"setdefault",
"__setitem__",
"__delitem__",
"__ior__",
):
self.assertFalse(hasattr(frozen, name), name)
class FrozenGlobalsTests(unittest.TestCase):
def setUp(self):
self.ns = frozendict(
__builtins__=builtins.__dict__,
__name__="frozen_test",
answer=42,
sys=sys,
)
def test_eval_and_implicit_scope(self):
self.assertIs(eval("globals()", self.ns), self.ns)
self.assertIs(eval("locals()", self.ns), self.ns)
self.assertEqual(eval("eval('answer')", self.ns), 42)
self.assertEqual(eval("(lambda: answer)()", self.ns), 42)
def test_function_and_materialized_frame(self):
local = {}
exec("def f():\n return sys._getframe(), globals(), answer\n", self.ns, local)
f = local["f"]
self.assertIs(f.__globals__, self.ns)
self.assertEqual(f.__module__, "frozen_test")
for _ in range(300):
frame, ns, answer = f()
self.assertIs(frame.f_globals, self.ns)
self.assertIs(ns, self.ns)
self.assertEqual(answer, 42)
gc.collect()
self.assertIs(frame.f_globals, self.ns)
def test_generator_frames(self):
local = {}
exec(
"def gen():\n yield sys._getframe(), globals(), answer\n yield answer\n",
self.ns,
local,
)
gen = local["gen"]()
self.assertIs(gen.gi_frame.f_globals, self.ns)
frame, ns, answer = next(gen)
self.assertIs(frame.f_globals, self.ns)
self.assertIs(ns, self.ns)
self.assertEqual(answer, 42)
self.assertEqual(next(gen), 42)
with self.assertRaises(StopIteration):
next(gen)
gc.collect()
self.assertIs(frame.f_globals, self.ns)
def test_coroutine_frames(self):
local = {}
exec(
"async def coro():\n return sys._getframe(), globals(), answer\n",
self.ns,
local,
)
coro = local["coro"]()
self.assertIs(coro.cr_frame.f_globals, self.ns)
with self.assertRaises(StopIteration) as stopped:
coro.send(None)
frame, ns, answer = stopped.exception.value
self.assertIs(frame.f_globals, self.ns)
self.assertIs(ns, self.ns)
self.assertEqual(answer, 42)
def test_async_generator_frames(self):
local = {}
exec(
"async def agen():\n yield sys._getframe(), globals(), answer\n",
self.ns,
local,
)
agen = local["agen"]()
self.assertIs(agen.ag_frame.f_globals, self.ns)
with self.assertRaises(StopIteration) as stopped:
agen.asend(None).send(None)
frame, ns, answer = stopped.exception.value
self.assertIs(frame.f_globals, self.ns)
self.assertIs(ns, self.ns)
self.assertEqual(answer, 42)
with self.assertRaises(StopIteration):
agen.aclose().send(None)
def test_local_namespace_and_imports(self):
local = {}
exec(
"import math\nclass C:\n value = answer\ndef f():\n import math\n return math.sqrt(answer)\n",
self.ns,
local,
)
self.assertEqual(local["C"].value, 42)
self.assertEqual(local["C"].__module__, "frozen_test")
self.assertEqual(local["f"](), local["math"].sqrt(42))
self.assertNotIn("math", self.ns)
def test_missing_builtins(self):
for func, source in [(eval, ""), (exec, "")]:
with self.assertRaises(TypeError) as caught:
func(source, frozendict())
self.assertEqual(
str(caught.exception),
"cannot assign __builtins__ to frozendict globals",
)
def test_writes_and_deletes(self):
for source, error in [
("answer = 0", "'frozendict' object does not support item assignment"),
(
"global answer; answer = 0",
"frozendict object does not support item assignment",
),
(
"global answer; del answer",
"frozendict object does not support item deletion",
),
(
"global missing; del missing",
"frozendict object does not support item deletion",
),
]:
with self.assertRaises(TypeError) as caught:
exec(source, self.ns)
self.assertEqual(str(caught.exception), error)
self.assertEqual(self.ns["answer"], 42)
with self.assertRaises(NameError):
exec("del answer", self.ns)
def test_explicit_function_constructor_stays_dict_only(self):
with self.assertRaises(TypeError):
types.FunctionType((lambda: 42).__code__, self.ns)
def test_subclass_name_and_global_lookup(self):
class F(frozendict):
def __getitem__(self, key):
if key == "answer":
return 81
return super().__getitem__(key)
ns = F(self.ns)
self.assertEqual(eval("answer", ns), 81)
self.assertEqual(eval("(lambda: answer)()", ns), 81)
self.assertEqual(eval("answer", ns, {}), 42)
def test_subclass_function_metadata_reads_storage(self):
class F(frozendict):
def __getitem__(self, key):
if key == "__name__":
return "wrong_name"
if key == "__builtins__":
return {"len": lambda x: 99}
return super().__getitem__(key)
ns = F(self.ns)
f = eval("lambda: len([])", ns)
self.assertEqual(f.__module__, "frozen_test")
self.assertIs(f.__globals__, ns)
self.assertEqual(f(), 0)
def test_subclass_builtins_assignment_hook(self):
calls = []
class F(frozendict):
def __setitem__(self, key, value):
calls.append((key, value))
for operation, source, mode, expected in (
(eval, "1", "eval", 1),
(exec, "pass", "exec", None),
):
namespace = F()
calls.clear()
self.assertEqual(
operation(compile(source, "<frozen-globals>", mode), namespace),
expected,
)
self.assertEqual(calls, [("__builtins__", builtins.__dict__)])
self.assertNotIn("__builtins__", namespace)
calls.clear()
with self.assertRaisesRegex(
TypeError, "frozendict object does not support item assignment"
):
operation(source, namespace)
self.assertEqual(calls, [("__builtins__", builtins.__dict__)])
def test_implicit_frozen_builtins_namespace_counts(self):
import _testinternalcapi
function = eval(
"lambda: len(())",
frozendict(__builtins__=frozendict(len=len)),
)
counts = _testinternalcapi.get_code_var_counts(function)
self.assertEqual(
counts["unbound"]["globals"],
{"total": 1, "numglobal": 0, "numbuiltin": 1, "numunknown": 0},
)
for argument in ("globalsns", "builtinsns"):
with self.assertRaises(TypeError):
_testinternalcapi.get_code_var_counts(
function, **{argument: frozendict()}
)
def test_warning_context_reads_frozen_storage(self):
import warnings
class F(frozendict):
def __getitem__(self, key):
if key in ("__warningregistry__", "__name__"):
raise AssertionError("warning context must read stored entries")
return super().__getitem__(key)
for has_registry in (False, True):
entries = dict(self.ns, warn=warnings.warn)
if has_registry:
entries["__warningregistry__"] = {}
function = eval("lambda: warn('probe')", F(entries))
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
if has_registry:
function()
self.assertEqual(len(caught), 1)
self.assertEqual(str(caught[0].message), "probe")
else:
with self.assertRaisesRegex(
TypeError,
"frozendict object does not support item assignment",
):
function()
self.assertEqual(caught, [])
def test_frozen_builtins(self):
ns = frozendict(__builtins__=frozendict({"len": len}))
self.assertEqual(eval("(lambda: len([1, 2]))()", ns), 2)
def test_shared_code_does_not_keep_mutable_global_specialization(self):
code = compile("lambda: answer", "<globals-probe>", "eval")
mutable = {"__builtins__": builtins.__dict__, "answer": 19}
f = eval(code, mutable)
for _ in range(300):
self.assertEqual(f(), 19)
frozen_f = eval(code, self.ns)
for _ in range(300):
self.assertEqual(frozen_f(), 42)
self.assertEqual(f(), 19)
mutable["answer"] = 23
self.assertEqual(f(), 23)
self.assertEqual(frozen_f(), 42)
frozen_suite = unittest.TestSuite(
unittest.defaultTestLoader.loadTestsFromTestCase(case)
for case in (FrozenDictDescriptorTests, FrozenGlobalsTests)
)
frozen_result = unittest.TextTestRunner(verbosity=2).run(frozen_suite)
assert frozen_result.wasSuccessful()
assert not frozen_result.skipped
+11
View File
@@ -8,6 +8,7 @@ Usage:
python scripts/update_lib migrate cpython/Lib/test/test_foo.py
python scripts/update_lib patches --from Lib/test/foo.py --to cpython/Lib/test/foo.py
python scripts/update_lib auto-mark Lib/test/test_foo.py
python scripts/update_lib copy-test SOURCE DESTINATION --test ClassName.method_name --source-sha256 HASH --destination-sha256 HASH
"""
import argparse
@@ -49,6 +50,11 @@ def main(argv: list[str] | None = None) -> int:
help="Copy library file/directory from CPython (delete existing first)",
add_help=False,
)
subparsers.add_parser(
"copy-test",
help="Copy one complete canonical test from hash-pinned inputs",
add_help=False,
)
subparsers.add_parser(
"deps",
help="Show dependency information for a module",
@@ -72,6 +78,11 @@ def main(argv: list[str] | None = None) -> int:
return copy_lib_main(remaining)
if args.command == "copy-test":
from update_lib.cmd_copy_test import main as copy_test_main
return copy_test_main(remaining)
if args.command == "migrate":
from update_lib.cmd_migrate import main as migrate_main
+134
View File
@@ -0,0 +1,134 @@
"""Copy one complete canonical test definition without refreshing its module."""
import argparse
import ast
import hashlib
import json
from pathlib import Path
from update_lib.patch_spec import _iter_class_methods, _iter_module_classes
def _definition(contents: str, qualified_name: str):
parts = qualified_name.split(".")
if len(parts) != 2 or not all(part.isidentifier() for part in parts):
raise ValueError("test name must be ClassName.method_name")
tree = ast.parse(contents)
classes = [node for node in _iter_module_classes(tree) if node.name == parts[0]]
if len(classes) != 1:
raise ValueError(f"Expected one class named {parts[0]}, found {len(classes)}")
methods = [
node for node in _iter_class_methods(classes[0]) if node.name == parts[1]
]
if len(methods) != 1:
raise ValueError(
f"Expected one method named {qualified_name}, found {len(methods)}"
)
method = methods[0]
parents = {
child: parent
for parent in ast.walk(tree)
for child in ast.iter_child_nodes(parent)
}
guards = []
child = method
while child in parents:
parent = parents[child]
if isinstance(parent, ast.If):
branch = "body" if child in parent.body else "orelse"
guards.append((ast.dump(parent.test), branch))
elif not isinstance(parent, (ast.Module, ast.ClassDef)):
raise ValueError("Only module/class scopes and if guards are supported")
child = parent
start = min([method.lineno, *(node.lineno for node in method.decorator_list)]) - 1
return method, guards, start, method.end_lineno
def copy_test(source: str, destination: str, qualified_name: str) -> str:
"""Replace an unambiguous whole definition, preserving all surrounding bytes.
Guard, signature, decorator and indentation differences require a broader
migration rather than silently changing the selected test's execution context.
"""
src, src_guards, src_start, src_end = _definition(source, qualified_name)
dst, dst_guards, dst_start, dst_end = _definition(destination, qualified_name)
if src_guards != dst_guards:
raise ValueError("Source and destination test guards differ")
def signature(node):
return (
ast.dump(node.args),
ast.dump(node.returns) if node.returns is not None else None,
[ast.dump(param) for param in getattr(node, "type_params", [])],
)
if type(src) is not type(dst) or signature(src) != signature(dst):
raise ValueError("Source and destination test signatures differ")
if [ast.dump(node) for node in src.decorator_list] != [
ast.dump(node) for node in dst.decorator_list
]:
raise ValueError(
"Source and destination decorators differ; preserve markers explicitly"
)
if src.col_offset != dst.col_offset:
raise ValueError("Source and destination indentation differs")
source_lines = source.splitlines(keepends=True)
destination_lines = destination.splitlines(keepends=True)
# AST equality omits comments, including project marker rationales. Require
# the complete decorator block to match before replacing the definition.
if (
source_lines[src_start : src.lineno - 1]
!= destination_lines[dst_start : dst.lineno - 1]
):
raise ValueError(
"Source and destination decorator text differs; preserve markers explicitly"
)
definition = source_lines[src_start:src_end]
result = "".join(
destination_lines[:dst_start] + definition + destination_lines[dst_end:]
)
_, _, result_start, result_end = _definition(result, qualified_name)
if result.splitlines(keepends=True)[result_start:result_end] != definition:
raise ValueError("Copied definition is not byte-identical to the source")
return result
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("source", type=Path)
parser.add_argument("destination", type=Path)
parser.add_argument("--test", required=True, help="Exact ClassName.method_name")
parser.add_argument("--source-sha256", required=True)
parser.add_argument("--destination-sha256", required=True)
args = parser.parse_args(argv)
try:
source = args.source.read_bytes()
destination = args.destination.read_bytes()
for label, contents, expected in (
("source", source, args.source_sha256),
("destination", destination, args.destination_sha256),
):
if hashlib.sha256(contents).hexdigest() != expected:
raise ValueError(f"{label} SHA256 does not match the pinned input")
result = copy_test(
source.decode("utf-8"), destination.decode("utf-8"), args.test
).encode("utf-8")
# Recheck the destination immediately before writing so stale invocations
# fail instead of overwriting an intervening edit.
if args.destination.read_bytes() != destination:
raise ValueError("Destination changed during test preparation")
args.destination.write_bytes(result)
except (OSError, UnicodeError, SyntaxError, ValueError) as exc:
parser.exit(1, f"Error: {exc}\n")
print(
json.dumps(
{
"test": args.test,
"source_sha256": args.source_sha256,
"destination_before_sha256": args.destination_sha256,
"destination_after_sha256": hashlib.sha256(result).hexdigest(),
},
sort_keys=True,
)
)
return 0
+47 -26
View File
@@ -197,7 +197,10 @@ class PatchEntryVisitor(ast.NodeVisitor):
patch = patch._replace(test_name="__self__")
self.patches.append(patch)
self.generic_visit(node)
# Keep conditional methods in this namespace, but do not enter
# nested classes or the bodies of methods.
for item in _iter_class_methods(node):
self.visit(item)
class PatchEntry(typing.NamedTuple):
@@ -228,22 +231,35 @@ class PatchEntry(typing.NamedTuple):
yield from visitor.patches
def _iter_module_classes(node: ast.AST) -> "Iterator[ast.ClassDef]":
"""Find module-scope classes, including conditional definitions.
Function-local and class-nested definitions have different namespaces and
cannot be addressed by the patch format's unqualified class names.
"""
for child in ast.iter_child_nodes(node):
if isinstance(child, ast.ClassDef):
yield child
elif not isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef, ast.Lambda)):
yield from _iter_module_classes(child)
def _iter_class_methods(
node: ast.AST,
) -> "Iterator[ast.FunctionDef | ast.AsyncFunctionDef]":
for child in ast.iter_child_nodes(node):
if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef)):
yield child
elif not isinstance(child, (ast.ClassDef, ast.Lambda)):
yield from _iter_class_methods(child)
def iter_tests(
tree: ast.Module,
) -> "Iterator[tuple[ast.ClassDef, ast.FunctionDef | ast.AsyncFunctionDef]]":
for key, nodes in ast.iter_fields(tree):
if key != "body":
continue
for cls_node in nodes:
if not isinstance(cls_node, ast.ClassDef):
continue
for fn_node in cls_node.body:
if not isinstance(fn_node, (ast.FunctionDef, ast.AsyncFunctionDef)):
continue
yield (cls_node, fn_node)
for cls_node in _iter_module_classes(tree):
for fn_node in _iter_class_methods(cls_node):
yield (cls_node, fn_node)
def iter_patches(contents: str) -> "Iterator[PatchEntry]":
@@ -286,24 +302,28 @@ def _iter_patch_lines(
# Track class bases for inherited async method lookup
class_bases: dict[str, list[str]] = {}
all_classes = set()
all_class_nodes = []
for node in tree.body:
if not isinstance(node, ast.ClassDef):
continue
all_class_nodes = list(_iter_module_classes(tree))
for node in all_class_nodes:
if node.name in all_classes:
raise ValueError(f"Ambiguous module-scope class name: {node.name}")
all_classes.add(node.name)
all_class_nodes.append(node)
method_counts = collections.Counter(
item.name for item in _iter_class_methods(node)
)
for method in patches.get(node.name, {}):
if method_counts[method] > 1:
raise ValueError(f"Ambiguous method name: {node.name}.{method}")
for node in tree.body:
for node in all_class_nodes:
if isinstance(node, ast.ClassDef):
cache[node.name] = node.end_lineno
cache[node.name] = (node.end_lineno, node.col_offset + len(DEFAULT_INDENT))
class_bases[node.name] = [
base.id
for base in node.bases
if isinstance(base, ast.Name) and base.id in all_classes
]
cls_async: set[str] = set()
for item in node.body:
for item in _iter_class_methods(node):
if isinstance(item, ast.AsyncFunctionDef):
cls_async.add(item.name)
if cls_async:
@@ -319,10 +339,11 @@ def _iter_patch_lines(
# Phase 2: Iterate and mark inherited tests
for cls_name, tests in sorted(patches.items()):
lineno = cache.get(cls_name)
if not lineno:
location = cache.get(cls_name)
if not location:
print(f"WARNING: {cls_name} does not exist in remote file", file=sys.stderr)
continue
lineno, indent = location
for test_name, specs in sorted(tests.items()):
if test_name == "__self__":
@@ -356,7 +377,7 @@ async def {test_name}(self):
def {test_name}(self):
{DEFAULT_INDENT}return super().{test_name}()
""".rstrip()
yield (lineno, textwrap.indent(patch_lines, DEFAULT_INDENT))
yield (lineno, textwrap.indent(patch_lines, " " * indent))
# Phase 3: Mark the class itself
for cls_node in all_class_nodes:
+206
View File
@@ -0,0 +1,206 @@
"""Tests for bounded, hash-pinned canonical test copying."""
import ast
import contextlib
import hashlib
import io
import tempfile
import unittest
from pathlib import Path
from update_lib.cmd_copy_test import copy_test, main
class TestCopyTest(unittest.TestCase):
destination = '''# Destination-only header
class Finder:
if int.__doc__:
def example():
r"""Count is below 800. π\\n"""
def untouched(self):
return "destination"
'''
source = '''# Canonical header must not be copied
class Finder:
if int.__doc__:
def example():
r"""Count is below 850. π\\n"""
def untouched(self):
return "canonical"
'''
def test_complete_definition_and_surroundings(self):
result = copy_test(self.source, self.destination, "Finder.example")
self.assertEqual(result, self.destination.replace("below 800", "below 850"))
self.assertIn('return "destination"', result)
source_method = ast.parse(self.source).body[0].body[0].body[0]
result_method = ast.parse(result).body[0].body[0].body[0]
self.assertEqual(
ast.get_source_segment(self.source, source_method),
ast.get_source_segment(result, result_method),
)
def test_guard_mismatch_is_rejected(self):
with self.assertRaisesRegex(ValueError, "guards differ"):
copy_test(
self.source.replace("if int.__doc__", "if enabled"),
self.destination,
"Finder.example",
)
def test_missing_or_ambiguous_definition_is_rejected(self):
for source in ("class Other: pass\n", self.source + self.source):
with self.subTest(source=source):
with self.assertRaisesRegex(ValueError, "Expected one class"):
copy_test(source, self.destination, "Finder.example")
duplicate = self.source.replace(
" def untouched(self):",
" def example():\n pass\n\n def untouched(self):",
)
with self.assertRaisesRegex(ValueError, "Expected one method"):
copy_test(duplicate, self.destination, "Finder.example")
def test_decorator_and_signature_changes_are_rejected(self):
decorated = self.source.replace(
" def example():", " @decorator\n def example():"
)
with self.assertRaisesRegex(ValueError, "decorators differ"):
copy_test(decorated, self.destination, "Finder.example")
with self.assertRaisesRegex(ValueError, "signatures differ"):
copy_test(
self.source.replace("def example():", "def example(arg):"),
self.destination,
"Finder.example",
)
def test_nested_local_scope_is_not_selected(self):
local = "def factory():\n" + "".join(
" " + line for line in self.source.splitlines(True)
)
with self.assertRaisesRegex(ValueError, "Expected one class"):
copy_test(local, self.destination, "Finder.example")
def test_decorator_comments_must_match_exactly(self):
for source_marker, destination_marker in (
(
"@unittest.expectedFailure",
"@unittest.expectedFailure # TODO: RUSTPYTHON; reason",
),
(
"@unittest.expectedFailure # original reason",
"@unittest.expectedFailure # changed reason",
),
(
"@unittest.expectedFailure\n @other",
"@unittest.expectedFailure\n # marker rationale\n @other",
),
(
"@unittest.expectedFailureIf(\n condition,\n 'reason',\n )",
"@unittest.expectedFailureIf(\n condition, # project limitation\n 'reason',\n )",
),
):
with self.subTest(destination_marker=destination_marker):
source = self.source.replace(
" def example():",
f" {source_marker}\n def example():",
)
destination = self.destination.replace(
" def example():",
f" {destination_marker}\n def example():",
)
with self.assertRaisesRegex(ValueError, "decorator text differs"):
copy_test(source, destination, "Finder.example")
def test_identical_decorator_comments_are_preserved(self):
prefix = (
" @unittest.expectedFailure # TODO: RUSTPYTHON; reason\n"
" # Keep this rationale with the marker.\n"
" def example():"
)
for declaration in ("def example():", "async def example():"):
with self.subTest(declaration=declaration):
source = self.source.replace(" def example():", prefix)
destination = self.destination.replace(" def example():", prefix)
source = source.replace("def example():", declaration)
destination = destination.replace("def example():", declaration)
self.assertEqual(
copy_test(source, destination, "Finder.example"),
destination.replace("below 800", "below 850"),
)
def test_cli_does_not_write_on_decorator_comment_mismatch(self):
with tempfile.TemporaryDirectory() as temp:
source = Path(temp) / "source.py"
destination = Path(temp) / "destination.py"
source.write_text(
self.source.replace(
" def example():",
" @unittest.expectedFailure\n def example():",
)
)
destination.write_text(
self.destination.replace(
" def example():",
" @unittest.expectedFailure # TODO: RUSTPYTHON; reason\n"
" def example():",
)
)
before = destination.read_bytes()
with (
contextlib.redirect_stderr(io.StringIO()) as errors,
self.assertRaises(SystemExit),
):
main(
[
str(source),
str(destination),
"--test",
"Finder.example",
"--source-sha256",
hashlib.sha256(source.read_bytes()).hexdigest(),
"--destination-sha256",
hashlib.sha256(before).hexdigest(),
]
)
self.assertIn("decorator text differs", errors.getvalue())
self.assertEqual(destination.read_bytes(), before)
def test_return_annotation_and_type_parameters_are_rejected(self):
for declaration in ("def example() -> int:", "def example[T]():"):
with self.subTest(declaration=declaration):
source = self.source.replace("def example():", declaration)
with self.assertRaisesRegex(ValueError, "signatures differ"):
copy_test(source, self.destination, "Finder.example")
def test_cli_hashes_and_no_write_on_stale_input(self):
with tempfile.TemporaryDirectory() as temp:
source = Path(temp) / "source.py"
destination = Path(temp) / "destination.py"
source.write_bytes(self.source.encode())
destination.write_bytes(self.destination.encode())
args = [
str(source),
str(destination),
"--test",
"Finder.example",
"--source-sha256",
hashlib.sha256(source.read_bytes()).hexdigest(),
"--destination-sha256",
"0" * 64,
]
with (
contextlib.redirect_stderr(io.StringIO()),
self.assertRaises(SystemExit),
):
main(args)
self.assertEqual(destination.read_bytes(), self.destination.encode())
args[-1] = hashlib.sha256(destination.read_bytes()).hexdigest()
with contextlib.redirect_stdout(io.StringIO()) as output:
self.assertEqual(main(args), 0)
self.assertIn('"source_sha256"', output.getvalue())
self.assertEqual(
destination.read_text(),
self.destination.replace("below 800", "below 850"),
)
+132
View File
@@ -67,6 +67,38 @@ class TestAsync(unittest.TestCase):
class TestExtractPatches(unittest.TestCase):
"""Tests for extract_patches function."""
def test_class_conditional_method_markers_are_preserved(self):
code = """class Owner:
if enabled:
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_value(self):
return 42
"""
patches = extract_patches(code)
self.assertIn("test_value", patches["Owner"])
clean = code.replace(
" @unittest.expectedFailure # TODO: RUSTPYTHON\n", ""
)
result = apply_patches(clean, patches)
self.assertIn(" @unittest.expectedFailure", result)
method = list(iter_tests(ast.parse(result)))[0][1]
self.assertEqual(method.col_offset, 8)
self.assertEqual(ast.dump(method.body[0]), "Return(value=Constant(value=42))")
def test_nested_class_markers_do_not_target_module_names(self):
code = """class Container:
class Target:
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_value(self):
pass
def factory():
class Target:
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_value(self):
pass
"""
self.assertEqual(extract_patches(code), {})
def test_extract_expected_failure(self):
"""Test extracting @unittest.expectedFailure decorator."""
code = f"""
@@ -177,6 +209,106 @@ class TestFoo(unittest.TestCase):
class TestApplyPatches(unittest.TestCase):
"""Tests for apply_patches function."""
def test_conditional_class_existing_method(self):
code = """import unittest
if enabled:
class TestConditional:
def test_value(self):
assert 1 + 1 == 2
"""
result = apply_patches(
code,
{
"TestConditional": {
"test_value": [
PatchSpec(
UtMethod.ExpectedFailureIf, "condition", "known limitation"
)
]
}
},
)
original = list(iter_tests(ast.parse(code)))[0][1]
updated = list(iter_tests(ast.parse(result)))[0][1]
self.assertEqual(ast.dump(original.body[0]), ast.dump(updated.body[0]))
self.assertEqual(updated.col_offset, 8)
self.assertIn(" @unittest.expectedFailureIf", result)
def test_conditional_class_inherited_override(self):
code = """import unittest
class Parent:
async def test_value(self):
return 42
if enabled:
class Child(Parent):
pass
"""
result = apply_patches(
code, {"Child": {"test_value": [PatchSpec(UtMethod.ExpectedSuccess)]}}
)
methods = list(iter_tests(ast.parse(result)))
self.assertEqual([cls.name for cls, _ in methods], ["Parent", "Child"])
self.assertIsInstance(methods[1][1], ast.AsyncFunctionDef)
self.assertIn(" return await super().test_value()", result)
self.assertIn(" @unittest.expectedSuccess", result)
def test_duplicate_conditional_class_names_are_rejected(self):
code = """if enabled:
class Duplicate:
pass
else:
class Duplicate:
pass
"""
with self.assertRaisesRegex(ValueError, "Ambiguous.*Duplicate"):
apply_patches(
code,
{"Duplicate": {"test_value": [PatchSpec(UtMethod.ExpectedFailure)]}},
)
def test_local_class_names_do_not_shadow_module_class(self):
code = """import unittest
class Target:
def test_value(self):
return 1
class Container:
class Target:
def test_value(self):
return 2
def factory():
class Target:
def test_value(self):
return 3
"""
result = apply_patches(
code, {"Target": {"test_value": [PatchSpec(UtMethod.ExpectedFailure)]}}
)
tree = ast.parse(result)
targets = [
node
for node in ast.walk(tree)
if isinstance(node, ast.ClassDef) and node.name == "Target"
]
self.assertEqual(
[len(node.body[0].decorator_list) for node in targets], [1, 0, 0]
)
def test_duplicate_conditional_method_names_are_rejected(self):
code = """class Owner:
if enabled:
def test_value(self):
return 1
else:
def test_value(self):
return 2
"""
with self.assertRaisesRegex(
ValueError, "Ambiguous method name: Owner.test_value"
):
apply_patches(
code, {"Owner": {"test_value": [PatchSpec(UtMethod.ExpectedFailure)]}}
)
def test_apply_expected_failure(self):
"""Test applying @unittest.expectedFailure."""
code = """import unittest
+4 -4
View File
@@ -1,14 +1,14 @@
#![cfg_attr(target_arch = "wasm32", allow(dead_code))]
use rustpython_vm::{
AsObject, PyResult, TryFromObject, VirtualMachine,
builtins::{PyDictRef, PyStrRef},
AsObject, PyRef, PyResult, TryFromObject, VirtualMachine,
builtins::{PyAnyDict, PyStrRef},
function::ArgIterable,
identifier,
};
pub(super) struct ShellHelper<'vm> {
vm: &'vm VirtualMachine,
globals: PyDictRef,
globals: PyRef<PyAnyDict>,
}
fn reverse_string(s: &mut String) {
@@ -54,7 +54,7 @@ fn split_idents_on_dot(line: &str) -> Option<(usize, Vec<String>)> {
}
impl<'vm> ShellHelper<'vm> {
pub(super) const fn new(vm: &'vm VirtualMachine, globals: PyDictRef) -> Self {
pub(super) const fn new(vm: &'vm VirtualMachine, globals: PyRef<PyAnyDict>) -> Self {
ShellHelper { vm, globals }
}