mirror of
https://github.com/RustPython/RustPython.git
synced 2026-10-28 18:59:04 +08:00
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:
1 parent
32ca8aa6f5
commit
373b0da216
3 files changed
+214
-4
No files matched your search
Vendored
-1
@@ -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'))
|
||||
|
||||
@@ -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")
|
||||
Reference in new issue
Block a user