diff --git a/Lib/test/test_pydoc/test_pydoc.py b/Lib/test/test_pydoc/test_pydoc.py index 116c0b82b..0dbd37010 100644 --- a/Lib/test/test_pydoc/test_pydoc.py +++ b/Lib/test/test_pydoc/test_pydoc.py @@ -1814,7 +1814,6 @@ cm(x) class method of test.test_pydoc.test_pydoc.X self.assertEqual(self._get_summary_line(type(sys.float_info).max), "max") - @unittest.expectedFailure # TODO: RUSTPYTHON @requires_docstrings def test_namedtuple_field_descriptor(self): Box = namedtuple('Box', ('width', 'height')) diff --git a/crates/vm/src/stdlib/_collections.rs b/crates/vm/src/stdlib/_collections.rs index a5136297d..3e5be342d 100644 --- a/crates/vm/src/stdlib/_collections.rs +++ b/crates/vm/src/stdlib/_collections.rs @@ -11,12 +11,14 @@ mod _collections { VirtualMachine, atomic_func, builtins::{ IterStatus::{Active, Exhausted}, - PositionIterInternal, PyDict, PyGenericAlias, PyInt, PyStr, PyType, PyTypeRef, + PositionIterInternal, PyDict, PyGenericAlias, PyInt, PyStr, PyTuple, PyType, PyTypeRef, locked_step, }, common::lock::{PyMutex, PyRwLock, PyRwLockReadGuard, PyRwLockWriteGuard}, convert::ToPyObject, - function::{FuncArgs, KwArgs, OptionalArg, PyComparisonValue, PySetterValue}, + function::{ + ArgIterable, FuncArgs, KwArgs, OptionalArg, PyComparisonValue, PySetterValue, PySsize, + }, object::{Traverse, TraverseFn}, protocol::{PyIter, PyIterReturn, PyMappingMethods, PyNumberMethods, PySequenceMethods}, recursion::ReprGuard, @@ -24,7 +26,8 @@ mod _collections { sliceable::SequenceIndexOp, types::{ AsMapping, AsNumber, AsSequence, Comparable, Constructor, DefaultConstructor, - Initializer, IterNext, Iterable, PyComparisonOp, Representable, SelfIter, + GetDescriptor, Initializer, IterNext, Iterable, PyComparisonOp, Representable, + SelfIter, }, utils::collection_repr, vm::MAX_MEMORY_SIZE, @@ -1127,4 +1130,152 @@ mod _collections { &AS_NUMBER } } + + #[pyfunction] + fn _count_elements( + mapping: PyObjectRef, + iterable: ArgIterable, + vm: &VirtualMachine, + ) -> PyResult<()> { + let iter = iterable.iter(vm)?; + let one = vm.ctx.new_int(1); + let get_name = vm.ctx.intern_str("get"); + let dict_type = vm.ctx.types.dict_type; + let class = mapping.class(); + let inherited_from_dict = |name| { + class + .get_attr(name) + .zip(dict_type.get_attr(name)) + .is_some_and(|(own, dict)| own.is(&dict)) + }; + // A dict whose type keeps `dict.get`/`dict.__setitem__` is updated in place, + // hashing each key once and never consulting `__missing__`. + if let Some(dict) = mapping.downcast_ref::() + && inherited_from_dict(get_name) + && inherited_from_dict(identifier!(vm, __setitem__)) + { + let entries = dict._as_dict_inner(); + for key in iter { + let key = key?; + let hash = key.hash(vm)?; + let count = match entries.get_known_hash(vm, &*key, hash)? { + Some(old) => vm._add(&old, one.as_object())?, + None => one.clone().into(), + }; + entries.insert_known_hash(vm, &*key, hash, count)?; + } + return Ok(()); + } + let get = mapping.get_attr(get_name, vm)?; + let zero: PyObjectRef = vm.ctx.new_int(0).into(); + for key in iter { + let key = key?; + let old = get.call((key.clone(), zero.clone()), vm)?; + let count = vm._add(&old, one.as_object())?; + mapping.set_item(&*key, count, vm)?; + } + Ok(()) + } + + #[pyattr] + #[pyclass(module = "collections", name = "_tuplegetter", traverse)] + #[derive(Debug, PyPayload)] + struct PyTupleGetter { + #[pytraverse(skip)] + index: isize, + doc: PyRwLock>, + } + + impl Constructor for PyTupleGetter { + type Args = (PySsize, PyObjectRef); + + fn py_new( + _cls: &Py, + (index, doc): Self::Args, + _vm: &VirtualMachine, + ) -> PyResult { + Ok(Self { + index, + doc: PyRwLock::new(Some(doc)), + }) + } + } + + #[pyclass(with(Constructor, GetDescriptor, Representable))] + impl PyTupleGetter { + fn doc(&self, vm: &VirtualMachine) -> PyObjectRef { + self.doc.read().clone().unwrap_or_else(|| vm.ctx.none()) + } + + #[pymember(type = "object")] + fn __doc__(vm: &VirtualMachine, zelf: PyObjectRef) -> PyResult { + let zelf: &Py = zelf.try_to_value(vm)?; + Ok(zelf.doc(vm)) + } + + #[pymember(type = "object", setter)] + fn set___doc__( + vm: &VirtualMachine, + zelf: PyObjectRef, + value: PySetterValue, + ) -> PyResult<()> { + let zelf: &Py = zelf.try_to_value(vm)?; + *zelf.doc.write() = match value { + PySetterValue::Assign(doc) => Some(doc), + PySetterValue::Delete => None, + }; + Ok(()) + } + + #[pyslot] + fn descr_set( + _zelf: &PyObject, + _obj: PyObjectRef, + value: PySetterValue, + vm: &VirtualMachine, + ) -> PyResult<()> { + Err(vm.new_attribute_error(match value { + PySetterValue::Assign(_) => "can't set attribute", + PySetterValue::Delete => "can't delete attribute", + })) + } + + #[pymethod] + fn __reduce__(zelf: PyRef, vm: &VirtualMachine) -> (PyTypeRef, (isize, PyObjectRef)) { + (zelf.class().to_owned(), (zelf.index, zelf.doc(vm))) + } + } + + impl GetDescriptor for PyTupleGetter { + fn descr_get( + zelf: &PyObject, + obj: Option<&PyObject>, + _cls: Option<&PyObject>, + vm: &VirtualMachine, + ) -> PyResult { + let (zelf, obj) = Self::_unwrap(zelf, obj, vm)?; + if vm.is_none(obj) { + return Ok(zelf.to_owned().into()); + } + let Some(tuple) = obj.downcast_ref::() else { + return Err(vm.new_type_error(format!( + "descriptor for index '{}' for tuple subclasses doesn't apply to '{}' object", + zelf.index, + obj.class().name() + ))); + }; + usize::try_from(zelf.index) + .ok() + .and_then(|index| tuple.as_slice().get(index)) + .cloned() + .ok_or_else(|| vm.new_index_error("tuple index out of range")) + } + } + + impl Representable for PyTupleGetter { + fn repr_str(zelf: &Py, vm: &VirtualMachine) -> PyResult { + let doc = zelf.doc(vm).repr(vm)?; + Ok(format!("{}({}, {})", zelf.class().name(), zelf.index, doc)) + } + } } diff --git a/extra_tests/snippets/stdlib_collections.py b/extra_tests/snippets/stdlib_collections.py index 8a9bdb453..e4602d1c9 100644 --- a/extra_tests/snippets/stdlib_collections.py +++ b/extra_tests/snippets/stdlib_collections.py @@ -56,3 +56,63 @@ b = BadRepr() d = deque([1, b, 2]) b.d = d repr(d) + +# namedtuple fields are `_tuplegetter` descriptors +import pickle +from _collections import _count_elements, _tuplegetter +from collections import Counter, OrderedDict, defaultdict, namedtuple + +from testutils import assert_raises + +Point = namedtuple("Point", "x y") +p = Point(1, 2) +assert type(Point.x) is _tuplegetter +assert (p.x, p.y) == (1, 2) +assert repr(Point.x) == "_tuplegetter(0, 'Alias for field number 0')" +assert Point.x.__get__(None, Point) is Point.x +assert Point.y.__get__((5, 6)) == 6 +assert pickle.loads(pickle.dumps(Point.y)).__get__((5, 6)) == 6 +with assert_raises(TypeError): + Point.x.__get__([1, 2]) +with assert_raises(IndexError): + _tuplegetter(3, None).__get__((1, 2)) +with assert_raises(AttributeError): + p.x = 3 +with assert_raises(AttributeError): + del p.x +Point.x.__doc__ = "The x-coordinate" +assert repr(Point.x) == "_tuplegetter(0, 'The x-coordinate')" + +# `_count_elements` behind Counter +counts = {} +_count_elements(counts, "abca") +assert counts == {"a": 2, "b": 1, "c": 1} +assert Counter("abracadabra").most_common(1) == [("a", 5)] +ordered = OrderedDict() +_count_elements(ordered, "bab") +assert list(ordered.items()) == [("b", 2), ("a", 1)] +factory = defaultdict(lambda: 100) +_count_elements(factory, "a") +assert factory == {"a": 1} + + +class Scaled(dict): + def __setitem__(self, key, value): + super().__setitem__(key, value * 10) + + +scaled = Scaled() +_count_elements(scaled, "aab") +assert scaled == {"a": 110, "b": 10} + + +class CustomGet(dict): + def get(self, key, default=None): + return 100 + + +custom_get = CustomGet() +_count_elements(custom_get, "aab") +assert custom_get == {"a": 101, "b": 101} +with assert_raises(AttributeError): + _count_elements([], "a")