Combine current main and validated mapping prerequisite

Assisted-by: OpenAI Codex: model identifier unavailable
This commit is contained in:
dot committed 2026-10-13 23:01:41 +09:00
commit 63c96dcecf
36 files changed
+3173 -395

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
-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')
-2
View File
@@ -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():
+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
+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;
+10 -1
View File
@@ -2,7 +2,7 @@ use crate::{
builtins::{PyBoundMethod, PyFunction},
function::{FuncArgs, IntoFuncArgs},
types::{GenericMethod, VectorCallFunc},
{PyObject, PyObjectRef, PyResult, VirtualMachine},
{AsObject, PyObject, PyObjectRef, PyResult, VirtualMachine},
};
impl PyObject {
@@ -78,6 +78,15 @@ impl<'a> PyCallable<'a> {
pub fn invoke(&self, args: impl IntoFuncArgs, vm: &VirtualMachine) -> PyResult {
let args = args.into_args(vm);
// The frozendict identity optimization belongs to the ordinary call path, not
// to __new__ or an explicit type.__call__. CALL_FUNCTION_EX reaches this path.
if self.obj.is(vm.ctx.types.frozendict_type)
&& args.kwargs.is_empty()
&& self.vectorcall.is_some()
{
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},
@@ -436,21 +436,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 +462,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 +479,27 @@ mod builtins {
),
};
let scope = crate::scope::Scope::with_builtins(Some(locals), globals, vm);
let builtins = globals.inner_getitem_opt(identifier!(vm, __builtins__), vm)?;
if builtins.is_none() {
if globals.is_frozen() {
let builtins = crate::frame::current_builtins()
.unwrap_or_else(|| vm.builtins.dict().into());
globals
.as_object()
.set_item(identifier!(vm, __builtins__), builtins, vm)
.map_err(|_| {
vm.new_type_error("cannot assign __builtins__ to frozendict globals")
})?;
} else {
globals.set_item(
identifier!(vm, __builtins__),
vm.builtins.dict().into(),
vm,
)?;
}
}
let scope = crate::scope::Scope::new(Some(locals), globals);
Ok(scope)
}
}
@@ -650,6 +668,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 +694,20 @@ mod builtins {
)));
}
// The source-string path repeats the stored-builtins check through the
// dictionary API, unlike direct evaluation of an existing code object.
if from_string
&& scope.globals.is_frozen()
&& 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,
@@ -3113,7 +3113,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() };
@@ -3226,7 +3226,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)
}
+12 -7
View File
@@ -762,12 +762,19 @@ 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 get_global = |name: &str| {
if globals.is_frozen() {
globals.inner_getitem_opt(name, vm)
} else {
Ok(globals.get_item(name, vm).ok())
}
};
let registry = match get_global("__warningregistry__")? {
Some(r) => r,
None => {
let r = vm.ctx.new_dict();
globals.set_item("__warningregistry__", r.clone().into(), vm)?;
r.into()
@@ -775,8 +782,6 @@ fn setup_context(
};
// Setup module.
let module = globals
.get_item("__name__", vm)
.unwrap_or_else(|_| vm.new_pyobj("<string>"));
let module = get_global("__name__")?.unwrap_or_else(|| vm.new_pyobj("<string>"));
Ok((filename.to_owned(), lineno, Some(module), registry))
}
File diff suppressed because it is too large. Load diff
+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 }
}