Visitar URL original
Implement PEP 814 frozendict · RustPython/RustPython@b203807 · GitHub
Skip to content

Commit b203807

Browse files
committed
Implement PEP 814 frozendict
Add dictionary-owned frozen mapping storage, shared views and cached hashing, read-only execution globals, type namespace support, and stable dictionary C-API coverage. Add differential regressions against CPython 3.15rc2. Assisted-by: Codex:model-version-unavailable
1 parent ada43c4 commit b203807

32 files changed

Lines changed: 2823 additions & 333 deletions

‎Lib/_collections_abc.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -823,6 +823,7 @@ def __eq__(self, other):
823823

824824
__reversed__ = None
825825

826+
Mapping.register(frozendict)
826827
Mapping.register(mappingproxy)
827828
Mapping.register(framelocalsproxy)
828829

‎Lib/copy.py‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ def copy(x):
101101

102102

103103
_copy_atomic_types = {types.NoneType, int, float, bool, complex, str, tuple,
104-
bytes, frozenset, type, range, slice, property,
104+
bytes, frozendict, frozenset, type, range, slice, property,
105105
types.BuiltinFunctionType, types.EllipsisType,
106106
types.NotImplementedType, types.FunctionType, types.CodeType,
107107
weakref.ref, super}
@@ -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

‎Lib/test/test_dict.py‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -951,7 +951,6 @@ def test_empty_presized_dict_in_freelist(self):
951951
'f': None, 'g': None, 'h': None}
952952
d = {}
953953

954-
@unittest.expectedFailure # TODO: RUSTPYTHON
955954
def test_container_iterator(self):
956955
# Bug #3680: tp_traverse was not implemented for dictiter and
957956
# dictview objects.
@@ -1264,7 +1263,6 @@ def test_free_after_iterating(self):
12641263
support.check_free_after_iterating(self, lambda d: iter(d.values()), dict)
12651264
support.check_free_after_iterating(self, lambda d: iter(d.items()), dict)
12661265

1267-
@unittest.expectedFailure # TODO: RUSTPYTHON
12681266
def test_equal_operator_modifying_operand(self):
12691267
# test fix for seg fault reported in bpo-27945 part 3.
12701268
class X():

‎crates/capi/src/ceval.rs‎

Lines changed: 124 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use crate::pystate::with_vm;
33
use crate::unicodeobject::decode_fsdefault_and_size;
44
use crate::util::FfiPtrExt;
55
use core::ffi::{CStr, c_char, c_int};
6-
use rustpython_vm::builtins::{PyCode, PyDict};
6+
use rustpython_vm::builtins::{PyAnyDictRef, PyCode};
77
use rustpython_vm::function::ArgMapping;
88
use rustpython_vm::scope::Scope;
99
use rustpython_vm::{AsObject, PyObject, TryFromObject};
@@ -32,12 +32,15 @@ pub unsafe extern "C" fn PyEval_EvalCode(
3232
) -> *mut PyObject {
3333
with_vm(|vm| {
3434
let code = unsafe { co.assume_borrowed_and_cast::<PyCode>(vm) }?;
35-
let globals = unsafe { globals.assume_borrowed_and_cast::<PyDict>(vm) }?;
35+
let globals =
36+
PyAnyDictRef::try_from_object(vm, unsafe { globals.assume_borrowed() }.to_owned())?;
3637
let locals = unsafe { locals.assume_borrowed_or_opt() }
3738
.map(|obj| ArgMapping::try_from_object(vm, obj.to_owned()))
3839
.transpose()?;
3940

40-
let scope = Scope::with_builtins(locals, globals.to_owned(), vm);
41+
let locals = locals.or_else(|| Some(ArgMapping::from_anydict_exact(globals.clone())));
42+
// Unlike Python's eval/exec, this API does not insert __builtins__.
43+
let scope = Scope::new(locals, globals);
4144

4245
vm.run_code_obj(code.to_owned(), scope)
4346
})
@@ -268,4 +271,122 @@ assert not hidden_leaked
268271
.unwrap();
269272
})
270273
}
274+
275+
#[pyfunction]
276+
fn frame_global_views(py: Python<'_>) -> PyResult<Vec<Bound<'_, PyAny>>> {
277+
unsafe {
278+
let frame = super::PyEval_GetFrame();
279+
Ok(vec![
280+
Bound::from_borrowed_ptr(py, super::PyEval_GetGlobals().cast()),
281+
Bound::from_owned_ptr_or_err(py, super::PyEval_GetFrameGlobals().cast())?,
282+
Bound::from_owned_ptr_or_err(py, crate::pyframe::PyFrame_GetGlobals(frame).cast())?,
283+
])
284+
}
285+
}
286+
287+
fn eval_with_globals<'py>(
288+
globals: &Bound<'py, PyAny>,
289+
source: &str,
290+
mode: &str,
291+
locals: Option<&Bound<'py, PyAny>>,
292+
) -> PyResult<Bound<'py, PyAny>> {
293+
let py = globals.py();
294+
let code = py.import("builtins")?.getattr("compile")?.call1((
295+
source,
296+
"<capi-frozendict>",
297+
mode,
298+
))?;
299+
unsafe {
300+
Bound::from_owned_ptr_or_err(
301+
py,
302+
pyo3::ffi::PyEval_EvalCode(
303+
code.as_ptr(),
304+
globals.as_ptr(),
305+
locals.map_or(core::ptr::null_mut(), Bound::as_ptr),
306+
),
307+
)
308+
}
309+
}
310+
311+
#[test]
312+
fn code_eval_frozen_globals() {
313+
Python::attach(|py| {
314+
let globals = py.eval(c"frozendict(answer=40)", None, None).unwrap();
315+
let result = eval_with_globals(&globals, "answer + len((1, 2))", "eval", None).unwrap();
316+
assert_eq!(result.extract::<i32>().unwrap(), 42);
317+
let identity = eval_with_globals(&globals, "globals()", "eval", None).unwrap();
318+
assert!(identity.is(&globals));
319+
assert!(!globals.contains("__builtins__").unwrap());
320+
321+
let namespace = pyo3::types::PyDict::new(py);
322+
namespace
323+
.set_item(
324+
"frame_global_views",
325+
wrap_pyfunction!(frame_global_views, py).unwrap(),
326+
)
327+
.unwrap();
328+
let globals = py
329+
.import("builtins")
330+
.unwrap()
331+
.getattr("frozendict")
332+
.unwrap()
333+
.call1((namespace,))
334+
.unwrap();
335+
let views = eval_with_globals(&globals, "frame_global_views()", "eval", None).unwrap();
336+
for view in views.try_iter().unwrap() {
337+
assert!(view.unwrap().is(&globals));
338+
}
339+
340+
let explicit = py
341+
.eval(
342+
c"frozendict(answer=40, __builtins__=frozendict(len=lambda x: 9))",
343+
None,
344+
None,
345+
)
346+
.unwrap();
347+
let result = eval_with_globals(&explicit, "answer + len(())", "eval", None).unwrap();
348+
assert_eq!(result.extract::<i32>().unwrap(), 49);
349+
})
350+
}
351+
352+
#[test]
353+
fn code_eval_frozen_globals_mutation() {
354+
Python::attach(|py| {
355+
let globals = py.eval(c"frozendict(answer=40)", None, None).unwrap();
356+
for source in [
357+
"answer = 1",
358+
"global answer; answer = 1",
359+
"global answer; del answer",
360+
] {
361+
let error = eval_with_globals(&globals, source, "exec", None).unwrap_err();
362+
assert!(error.is_instance_of::<pyo3::exceptions::PyTypeError>(py));
363+
}
364+
let locals = pyo3::types::PyDict::new(py);
365+
eval_with_globals(
366+
&globals,
367+
"local_answer = answer + 2",
368+
"exec",
369+
Some(locals.as_any()),
370+
)
371+
.unwrap();
372+
assert_eq!(
373+
locals
374+
.get_item("local_answer")
375+
.unwrap()
376+
.unwrap()
377+
.extract::<i32>()
378+
.unwrap(),
379+
42
380+
);
381+
assert_eq!(
382+
globals
383+
.get_item("answer")
384+
.unwrap()
385+
.extract::<i32>()
386+
.unwrap(),
387+
40
388+
);
389+
assert!(!globals.contains("__builtins__").unwrap());
390+
})
391+
}
271392
}

0 commit comments

Comments
 (0)