Skip to content

Commit 28393e9

Browse files
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>
1 parent bef501f commit 28393e9

40 files changed

Lines changed: 2705 additions & 493 deletions

‎Lib/copy.py‎

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -100,14 +100,14 @@ def copy(x):
100100
return _reconstruct(x, None, *rv)
101101

102102

103-
_copy_atomic_types = {types.NoneType, int, float, bool, complex, str, tuple,
104-
bytes, frozenset, type, range, slice, property,
103+
_copy_atomic_types = frozenset({types.NoneType, int, float, bool, complex, str, tuple,
104+
bytes, frozendict, frozenset, type, range, slice, property,
105105
types.BuiltinFunctionType, types.EllipsisType,
106106
types.NotImplementedType, types.FunctionType, types.CodeType,
107-
weakref.ref, super}
108-
_copy_builtin_containers = {list, dict, set, bytearray}
107+
weakref.ref, super})
108+
_copy_builtin_containers = frozenset({list, dict, set, bytearray})
109109

110-
def deepcopy(x, memo=None, _nil=[]):
110+
def deepcopy(x, memo=None):
111111
"""Deep copy operation on arbitrary Python objects.
112112
113113
See the module's __doc__ string for more info.
@@ -122,8 +122,8 @@ def deepcopy(x, memo=None, _nil=[]):
122122
if memo is None:
123123
memo = {}
124124
else:
125-
y = memo.get(d, _nil)
126-
if y is not _nil:
125+
y = memo.get(d, None)
126+
if y is not None:
127127
return y
128128

129129
copier = _deepcopy_dispatch.get(cls)
@@ -162,9 +162,9 @@ def deepcopy(x, memo=None, _nil=[]):
162162
_keep_alive(x, memo) # Make sure x lives at least as long as d
163163
return y
164164

165-
_atomic_types = {types.NoneType, types.EllipsisType, types.NotImplementedType,
165+
_atomic_types = frozenset({types.NoneType, types.EllipsisType, types.NotImplementedType,
166166
int, float, bool, complex, bytes, str, types.CodeType, type, range,
167-
types.BuiltinFunctionType, types.FunctionType, weakref.ref, property}
167+
types.BuiltinFunctionType, types.FunctionType, weakref.ref, property})
168168

169169
_deepcopy_dispatch = d = {}
170170

@@ -203,6 +203,21 @@ def _deepcopy_dict(x, memo, deepcopy=deepcopy):
203203
return y
204204
d[dict] = _deepcopy_dict
205205

206+
def _deepcopy_frozendict(x, memo, deepcopy=deepcopy):
207+
y = {}
208+
for key, value in x.items():
209+
y[deepcopy(key, memo)] = deepcopy(value, memo)
210+
211+
# We're not going to put the frozendict in the memo, but it's still
212+
# important we check for it, in case the frozendict contains recursive
213+
# mutable structures.
214+
try:
215+
return memo[id(x)]
216+
except KeyError:
217+
pass
218+
return frozendict(y)
219+
d[frozendict] = _deepcopy_frozendict
220+
206221
def _deepcopy_method(x, memo): # Copy instance methods
207222
return type(x)(x.__func__, deepcopy(x.__self__, memo))
208223
d[types.MethodType] = _deepcopy_method
@@ -230,7 +245,7 @@ def _reconstruct(x, memo, func, args,
230245
*, deepcopy=deepcopy):
231246
deep = memo is not None
232247
if deep and args:
233-
args = (deepcopy(arg, memo) for arg in args)
248+
args = [deepcopy(arg, memo) for arg in args]
234249
y = func(*args)
235250
if deep:
236251
memo[id(x)] = y

‎Lib/test/mapping_tests.py‎

Lines changed: 70 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from test import support
55

66

7-
class BasicTestMappingProtocol(unittest.TestCase):
7+
class BasicTestImmutableMappingProtocol(unittest.TestCase):
88
# This base class can be used to check that an object conforms to the
99
# mapping protocol
1010

@@ -20,12 +20,9 @@ def _empty_mapping(self):
2020
"""Return an empty mapping object"""
2121
return self.type2test()
2222
def _full_mapping(self, data):
23-
"""Return a mapping object with the value contained in data
23+
"""Return a mapping object with the values contained in data
2424
dictionary"""
25-
x = self._empty_mapping()
26-
for key, value in data.items():
27-
x[key] = value
28-
return x
25+
return self.type2test(data)
2926

3027
def __init__(self, *args, **kw):
3128
unittest.TestCase.__init__(self, *args, **kw)
@@ -88,6 +85,72 @@ def check_iterandlist(iter, lst, ref):
8885
self.assertEqual(d.get(knownkey, knownvalue), knownvalue)
8986
self.assertNotIn(knownkey, d)
9087

88+
def test_constructor(self):
89+
self.assertEqual(self._empty_mapping(), self._empty_mapping())
90+
91+
def test_bool(self):
92+
self.assertTrue(not self._empty_mapping())
93+
self.assertTrue(self.reference)
94+
self.assertFalse(bool(self._empty_mapping()))
95+
self.assertTrue(bool(self.reference))
96+
97+
def test_keys(self):
98+
d = self._empty_mapping()
99+
self.assertEqual(list(d.keys()), [])
100+
d = self.reference
101+
self.assertIn(list(self.inmapping.keys())[0], d.keys())
102+
self.assertNotIn(list(self.other.keys())[0], d.keys())
103+
self.assertRaises(TypeError, d.keys, None)
104+
105+
def test_values(self):
106+
d = self._empty_mapping()
107+
self.assertEqual(list(d.values()), [])
108+
109+
self.assertRaises(TypeError, d.values, None)
110+
111+
def test_items(self):
112+
d = self._empty_mapping()
113+
self.assertEqual(list(d.items()), [])
114+
115+
self.assertRaises(TypeError, d.items, None)
116+
117+
def test_len(self):
118+
d = self._empty_mapping()
119+
self.assertEqual(len(d), 0)
120+
121+
def test_getitem(self):
122+
d = self.reference
123+
self.assertEqual(d[list(self.inmapping.keys())[0]],
124+
list(self.inmapping.values())[0])
125+
126+
self.assertRaises(TypeError, d.__getitem__)
127+
128+
# no test_fromkeys or test_copy as both os.environ and selves don't support it
129+
130+
def test_get(self):
131+
d = self._empty_mapping()
132+
self.assertIsNone(d.get(list(self.other.keys())[0]))
133+
self.assertEqual(d.get(list(self.other.keys())[0], 3), 3)
134+
d = self.reference
135+
self.assertIsNone(d.get(list(self.other.keys())[0]))
136+
self.assertEqual(d.get(list(self.other.keys())[0], 3), 3)
137+
self.assertEqual(d.get(list(self.inmapping.keys())[0]),
138+
list(self.inmapping.values())[0])
139+
self.assertEqual(d.get(list(self.inmapping.keys())[0], 3),
140+
list(self.inmapping.values())[0])
141+
self.assertRaises(TypeError, d.get)
142+
self.assertRaises(TypeError, d.get, None, None, None)
143+
144+
145+
class BasicTestMappingProtocol(BasicTestImmutableMappingProtocol):
146+
def _full_mapping(self, data):
147+
"""Return a mapping object with the values contained in data
148+
dictionary"""
149+
x = self._empty_mapping()
150+
for key, value in data.items():
151+
x[key] = value
152+
return x
153+
91154
def test_write(self):
92155
# Test for write operations on mapping
93156
p = self._empty_mapping()
@@ -130,46 +193,6 @@ def test_write(self):
130193
p=self._empty_mapping()
131194
self.assertRaises(KeyError, p.popitem)
132195

133-
def test_constructor(self):
134-
self.assertEqual(self._empty_mapping(), self._empty_mapping())
135-
136-
def test_bool(self):
137-
self.assertTrue(not self._empty_mapping())
138-
self.assertTrue(self.reference)
139-
self.assertTrue(bool(self._empty_mapping()) is False)
140-
self.assertTrue(bool(self.reference) is True)
141-
142-
def test_keys(self):
143-
d = self._empty_mapping()
144-
self.assertEqual(list(d.keys()), [])
145-
d = self.reference
146-
self.assertIn(list(self.inmapping.keys())[0], d.keys())
147-
self.assertNotIn(list(self.other.keys())[0], d.keys())
148-
self.assertRaises(TypeError, d.keys, None)
149-
150-
def test_values(self):
151-
d = self._empty_mapping()
152-
self.assertEqual(list(d.values()), [])
153-
154-
self.assertRaises(TypeError, d.values, None)
155-
156-
def test_items(self):
157-
d = self._empty_mapping()
158-
self.assertEqual(list(d.items()), [])
159-
160-
self.assertRaises(TypeError, d.items, None)
161-
162-
def test_len(self):
163-
d = self._empty_mapping()
164-
self.assertEqual(len(d), 0)
165-
166-
def test_getitem(self):
167-
d = self.reference
168-
self.assertEqual(d[list(self.inmapping.keys())[0]],
169-
list(self.inmapping.values())[0])
170-
171-
self.assertRaises(TypeError, d.__getitem__)
172-
173196
def test_update(self):
174197
# mapping argument
175198
d = self._empty_mapping()
@@ -265,22 +288,6 @@ def __next__(self):
265288

266289
self.assertRaises(ValueError, d.update, [(1, 2, 3)])
267290

268-
# no test_fromkeys or test_copy as both os.environ and selves don't support it
269-
270-
def test_get(self):
271-
d = self._empty_mapping()
272-
self.assertTrue(d.get(list(self.other.keys())[0]) is None)
273-
self.assertEqual(d.get(list(self.other.keys())[0], 3), 3)
274-
d = self.reference
275-
self.assertTrue(d.get(list(self.other.keys())[0]) is None)
276-
self.assertEqual(d.get(list(self.other.keys())[0], 3), 3)
277-
self.assertEqual(d.get(list(self.inmapping.keys())[0]),
278-
list(self.inmapping.values())[0])
279-
self.assertEqual(d.get(list(self.inmapping.keys())[0], 3),
280-
list(self.inmapping.values())[0])
281-
self.assertRaises(TypeError, d.get)
282-
self.assertRaises(TypeError, d.get, None, None, None)
283-
284291
def test_setdefault(self):
285292
d = self._empty_mapping()
286293
self.assertRaises(TypeError, d.setdefault)
@@ -622,7 +629,7 @@ def __repr__(self):
622629
d = self._full_mapping({1: BadRepr()})
623630
self.assertRaises(Exc, repr, d)
624631

625-
@support.run_with_limited_c_stack()
632+
@support.skip_if_huge_c_stack()
626633
@support.skip_wasi_stack_overflow()
627634
@support.skip_emscripten_stack_overflow()
628635
@support.skip_if_sanitizer("requires deep stack", ub=True)

‎Lib/test/pickletester.py‎

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4394,8 +4394,6 @@ def test_fast_save_enter_frozenset(self):
43944394
def test_fast_save_enter_set(self):
43954395
self.fast_save_enter(lambda i: set([i]))
43964396

4397-
# TODO: RUSTPYTHON; frozendict support: https://github.com/RustPython/RustPython/pull/8951
4398-
@unittest.expectedFailure
43994397
def test_fast_save_enter_frozendict(self):
44004398
if self.py_version < (3, 15):
44014399
self.skipTest('need frozendict')
@@ -4443,8 +4441,6 @@ def test_deep_nested_struct_set(self):
44434441
depth=FAST_NESTING_LIMIT+1,
44444442
compare_equal=False)
44454443

4446-
# TODO: RUSTPYTHON; frozendict support: https://github.com/RustPython/RustPython/pull/8951
4447-
@unittest.expectedFailure
44484444
def test_deep_nested_struct_frozendict(self):
44494445
if self.py_version < (3, 15):
44504446
self.skipTest('need frozendict')

0 commit comments

Comments
 (0)