Add native _collections._tuplegetter and _collections._count_elements (#8835)

* Add native _collections._tuplegetter and _collections._count_elements

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

* ruff + fixed unittest

* use PySsize

* remove docstring comment

* cover "get()" overriding
This commit is contained in:
1ndahous3 authored and GitHub committed 2026-09-27 09:52:24 +09:00
1 parent 32ca8aa6f5
commit 373b0da216
3 files changed
+214 -4

No files matched your search

-1
View File
@@ -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'))
+154 -3
View File
@@ -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::<PyDict>()
&& 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<Option<PyObjectRef>>,
}
impl Constructor for PyTupleGetter {
type Args = (PySsize, PyObjectRef);
fn py_new(
_cls: &Py<PyType>,
(index, doc): Self::Args,
_vm: &VirtualMachine,
) -> PyResult<Self> {
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<Self> = 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<Self> = 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<Self>, 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::<PyTuple>() 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<Self>, vm: &VirtualMachine) -> PyResult<String> {
let doc = zelf.doc(vm).repr(vm)?;
Ok(format!("{}({}, {})", zelf.class().name(), zelf.index, doc))
}
}
}
@@ -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")