fix(core): align hashability of dict views, namespace, and typing builtins (#8844)

This commit is contained in:
Alessio Attilio authored and GitHub committed 2026-09-28 21:53:58 +09:00
1 parent 908368eb1c
commit 7329839fb5
11 files changed
+69 -29

No files matched your search

+5 -20
View File
@@ -7,11 +7,11 @@ mod _queue {
use std::time::Instant;
use crate::vm::{
AsObject, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine,
AsObject, Py, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine,
builtins::{PyBaseExceptionRef, PyException, PyGenericAlias, PyStr, PyType, PyTypeRef},
function::{PyComparisonValue, TimeoutSeconds},
function::TimeoutSeconds,
protocol::PyNumberMethods,
types::{AsNumber, Comparable, Constructor, PyComparisonOp, Representable},
types::{AsNumber, Constructor, Representable},
};
type BufInner = VecDeque<PyObjectRef>;
@@ -143,7 +143,7 @@ mod _queue {
}
#[pyattr]
#[pyclass(module = "_queue", name = "SimpleQueue", unhashable = true)]
#[pyclass(module = "_queue", name = "SimpleQueue")]
#[derive(Debug, PyPayload)]
struct PySimpleQueue {
buf: Buf,
@@ -227,7 +227,7 @@ mod _queue {
}
#[pyclass(
with(Constructor, Comparable, Representable),
with(Constructor, Representable),
flags(BASETYPE, HAS_WEAKREF, IMMUTABLETYPE)
)]
impl PySimpleQueue {
@@ -329,21 +329,6 @@ mod _queue {
}
}
impl Comparable for PySimpleQueue {
fn cmp(
zelf: &Py<Self>,
other: &PyObject,
op: PyComparisonOp,
_vm: &VirtualMachine,
) -> PyResult<PyComparisonValue> {
Ok(if let Some(res) = op.identical_optimization(zelf, other) {
res.into()
} else {
PyComparisonValue::NotImplemented
})
}
}
impl Representable for PySimpleQueue {
fn repr(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<PyRef<PyStr>> {
Ok(vm.ctx.new_str(format!(
+1 -1
View File
@@ -3298,7 +3298,7 @@ mod _ssl {
}
#[pyattr]
#[pyclass(module = "ssl", name = "SSLSession")]
#[pyclass(module = "ssl", name = "SSLSession", unhashable = true)]
#[derive(PyPayload)]
struct PySslSession {
session: *mut sys::SSL_SESSION,
+1 -1
View File
@@ -3979,7 +3979,7 @@ mod _ssl {
// SSLSession - represents a cached SSL session
// NOTE: This is an EMULATION - actual session data is managed by Rustls internally
#[pyattr]
#[pyclass(name = "SSLSession", module = "ssl")]
#[pyclass(name = "SSLSession", module = "ssl", unhashable = true)]
#[derive(Debug, PyPayload)]
struct PySSLSession {
context_identity: Arc<()>,
+1 -1
View File
@@ -737,7 +737,7 @@ mod _ssl {
}
#[pyattr]
#[pyclass(name = "SSLSession", module = "ssl")]
#[pyclass(name = "SSLSession", module = "ssl", unhashable = true)]
#[derive(Debug, PyPayload)]
struct PySSLSession {
session_id: Vec<u8>,
+5 -1
View File
@@ -1165,10 +1165,11 @@ macro_rules! dict_view {
$class_name: literal,
$iter_class_name: literal,
$reverse_iter_class_name: literal,
$unhashable: literal,
$project_fn: expr,
$result_fn: expr
) => {
#[pyclass(module = false, name = $class_name)]
#[pyclass(module = false, name = $class_name, unhashable = $unhashable)]
#[derive(Debug)]
pub(crate) struct $name {
pub(crate) dict: PyDictRef,
@@ -1456,6 +1457,7 @@ dict_view! {
"dict_keys",
"dict_keyiterator",
"dict_reversekeyiterator",
true,
|key: &PyObject, _value| key.to_owned(),
|_vm: &VirtualMachine, key: PyObjectRef| key
}
@@ -1470,6 +1472,7 @@ dict_view! {
"dict_values",
"dict_valueiterator",
"dict_reversevalueiterator",
false,
|_key: &PyObject, value: &PyObjectRef| value.clone(),
|_vm: &VirtualMachine, value: PyObjectRef| value
}
@@ -1484,6 +1487,7 @@ dict_view! {
"dict_items",
"dict_itemiterator",
"dict_reverseitemiterator",
true,
|key: &PyObject, value: &PyObjectRef| (key.to_owned(), value.clone()),
// Builds a tuple, so it runs after the dict's read guard is released.
|vm: &VirtualMachine, (key, value): (PyObjectRef, PyObjectRef)|
+1 -1
View File
@@ -11,7 +11,7 @@ use crate::{
};
use rustpython_common::wtf8::Wtf8Buf;
#[pyclass(module = "types", name = "SimpleNamespace")]
#[pyclass(module = "types", name = "SimpleNamespace", unhashable = true)]
// The dict lives in the object extension, not the payload. The offset is
// the dict cell at the front of that extension.
#[pymember(name = "__dict__", offset = ::rustpython_vm::object::dict_member_offset())]
@@ -978,7 +978,7 @@ pub(crate) mod ordered_dict {
}
#[pyattr]
#[pyclass(name = "odict_keys", traverse)]
#[pyclass(name = "odict_keys", unhashable = true, traverse)]
#[derive(Debug, PyPayload)]
struct PyODictKeys {
od: PyRef<PyOrderedDict>,
@@ -1122,7 +1122,7 @@ pub(crate) mod ordered_dict {
}
#[pyattr]
#[pyclass(name = "odict_items", traverse)]
#[pyclass(name = "odict_items", unhashable = true, traverse)]
#[derive(Debug, PyPayload)]
pub(crate) struct PyOrderedDictItems {
od: PyRef<PyOrderedDict>,
+2 -2
View File
@@ -845,7 +845,7 @@ pub(crate) mod typevar {
}
#[pyattr]
#[pyclass(name = "ParamSpecArgs", module = "typing")]
#[pyclass(name = "ParamSpecArgs", module = "typing", unhashable = true)]
#[derive(Debug, PyPayload)]
pub struct ParamSpecArgs {
#[pymember]
@@ -905,7 +905,7 @@ pub(crate) mod typevar {
}
#[pyattr]
#[pyclass(name = "ParamSpecKwargs", module = "typing")]
#[pyclass(name = "ParamSpecKwargs", module = "typing", unhashable = true)]
#[derive(Debug, PyPayload)]
pub struct ParamSpecKwargs {
#[pymember]
+25
View File
@@ -687,3 +687,28 @@ class MergeMapping(dict):
# Generic mappings retain their lookup hooks instead of exposing dict storage.
for merge in (dict, merge_with_update, merge_with_ior):
assert merge(MergeMapping(stored=0)) == {"virtual": 42}
# Test hashability of dict and OrderedDict views
import collections.abc
d = {"a": 1, "b": 2}
assert type(d.keys()).__hash__ is None
assert type(d.items()).__hash__ is None
assert type(d.values()).__hash__ is not None
assert not isinstance(d.keys(), collections.abc.Hashable)
assert not isinstance(d.items(), collections.abc.Hashable)
with assert_raises(TypeError):
hash(d.keys())
with assert_raises(TypeError):
hash(d.items())
od = collections.OrderedDict([("a", 1)])
assert type(od.keys()).__hash__ is None
assert type(od.items()).__hash__ is None
assert type(od.values()).__hash__ is not None
assert not isinstance(od.keys(), collections.abc.Hashable)
assert not isinstance(od.items(), collections.abc.Hashable)
with assert_raises(TypeError):
hash(od.keys())
with assert_raises(TypeError):
hash(od.items())
+7
View File
@@ -12,6 +12,13 @@ assert ns.b == "Rust"
with assert_raises(AttributeError):
_ = ns.c
import collections.abc
assert types.SimpleNamespace.__hash__ is None
assert not isinstance(ns, collections.abc.Hashable)
with assert_raises(TypeError):
hash(ns)
def _run_missing_type_params_regression():
args = _ast.arguments(
+19
View File
@@ -10,3 +10,22 @@ print(curry.__annotations__)
assert curry.__annotations__['foo'] is int
assert curry.__annotations__['return'] is float
assert curry.__annotations__['bla'] is int
import collections.abc
import typing
P = typing.ParamSpec('P')
assert P.args.__hash__ is None
assert P.kwargs.__hash__ is None
assert not isinstance(P.args, collections.abc.Hashable)
assert not isinstance(P.kwargs, collections.abc.Hashable)
try:
hash(P.args)
assert False, "expected TypeError"
except TypeError:
pass
try:
hash(P.kwargs)
assert False, "expected TypeError"
except TypeError:
pass