_typing.Union

This commit is contained in:
Jeong, YunWon
2026-01-19 19:02:09 +09:00
committed by Jeong, YunWon
parent 2bbb3d07c6
commit bb72418d9e
8 changed files with 293 additions and 110 deletions

4
Lib/copyreg.py vendored
View File

@@ -31,8 +31,8 @@ def pickle_complex(c):
pickle(complex, pickle_complex, complex)
def pickle_union(obj):
import functools, operator
return functools.reduce, (operator.or_, obj.__args__)
import typing, operator
return operator.getitem, (typing.Union, obj.__args__)
pickle(type(int | str), pickle_union)

View File

@@ -235,11 +235,11 @@ impl PyGenericAlias {
Err(vm.new_type_error("issubclass() argument 2 cannot be a parameterized generic"))
}
fn __ror__(zelf: PyObjectRef, other: PyObjectRef, vm: &VirtualMachine) -> PyObjectRef {
fn __ror__(zelf: PyObjectRef, other: PyObjectRef, vm: &VirtualMachine) -> PyResult {
type_::or_(other, zelf, vm)
}
fn __or__(zelf: PyObjectRef, other: PyObjectRef, vm: &VirtualMachine) -> PyObjectRef {
fn __or__(zelf: PyObjectRef, other: PyObjectRef, vm: &VirtualMachine) -> PyResult {
type_::or_(zelf, other, vm)
}
}
@@ -509,7 +509,7 @@ impl AsMapping for PyGenericAlias {
impl AsNumber for PyGenericAlias {
fn as_number() -> &'static PyNumberMethods {
static AS_NUMBER: PyNumberMethods = PyNumberMethods {
or: Some(|a, b, vm| Ok(PyGenericAlias::__or__(a.to_owned(), b.to_owned(), vm))),
or: Some(|a, b, vm| PyGenericAlias::__or__(a.to_owned(), b.to_owned(), vm)),
..PyNumberMethods::NOT_IMPLEMENTED
};
&AS_NUMBER

View File

@@ -20,7 +20,6 @@ use crate::{
borrow::BorrowedValue,
lock::{PyRwLock, PyRwLockReadGuard},
},
convert::ToPyResult,
function::{FuncArgs, KwArgs, OptionalArg, PyMethodDef, PySetterValue},
object::{Traverse, TraverseFn},
protocol::{PyIterReturn, PyNumberMethods},
@@ -1038,11 +1037,11 @@ impl PyType {
)
}
pub fn __ror__(zelf: PyObjectRef, other: PyObjectRef, vm: &VirtualMachine) -> PyObjectRef {
pub fn __ror__(zelf: PyObjectRef, other: PyObjectRef, vm: &VirtualMachine) -> PyResult {
or_(other, zelf, vm)
}
pub fn __or__(zelf: PyObjectRef, other: PyObjectRef, vm: &VirtualMachine) -> PyObjectRef {
pub fn __or__(zelf: PyObjectRef, other: PyObjectRef, vm: &VirtualMachine) -> PyResult {
or_(zelf, other, vm)
}
@@ -1850,7 +1849,7 @@ impl Callable for PyType {
impl AsNumber for PyType {
fn as_number() -> &'static PyNumberMethods {
static AS_NUMBER: PyNumberMethods = PyNumberMethods {
or: Some(|a, b, vm| or_(a.to_owned(), b.to_owned(), vm).to_pyresult(vm)),
or: Some(|a, b, vm| or_(a.to_owned(), b.to_owned(), vm)),
..PyNumberMethods::NOT_IMPLEMENTED
};
&AS_NUMBER
@@ -2013,9 +2012,9 @@ pub(crate) fn call_slot_new(
slot_new(subtype, args, vm)
}
pub(crate) fn or_(zelf: PyObjectRef, other: PyObjectRef, vm: &VirtualMachine) -> PyObjectRef {
pub(crate) fn or_(zelf: PyObjectRef, other: PyObjectRef, vm: &VirtualMachine) -> PyResult {
if !union_::is_unionable(zelf.clone(), vm) || !union_::is_unionable(other.clone(), vm) {
return vm.ctx.not_implemented();
return Ok(vm.ctx.not_implemented());
}
let tuple = PyTuple::new_ref(vec![zelf, other], &vm.ctx);

View File

@@ -2,10 +2,10 @@ use super::{genericalias, type_};
use crate::{
AsObject, Context, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine,
atomic_func,
builtins::{PyFrozenSet, PyGenericAlias, PyStr, PyTuple, PyTupleRef, PyType},
builtins::{PyFrozenSet, PySet, PyStr, PyTuple, PyTupleRef, PyType},
class::PyClassImpl,
common::hash,
convert::{ToPyObject, ToPyResult},
convert::ToPyObject,
function::PyComparisonValue,
protocol::{PyMappingMethods, PyNumberMethods},
stdlib::typing::TypeAliasType,
@@ -16,9 +16,13 @@ use std::sync::LazyLock;
const CLS_ATTRS: &[&str] = &["__module__"];
#[pyclass(module = "types", name = "UnionType", traverse)]
#[pyclass(module = "typing", name = "Union", traverse)]
pub struct PyUnion {
args: PyTupleRef,
/// Frozenset of hashable args, or None if all args were hashable
hashable_args: Option<PyRef<PyFrozenSet>>,
/// Tuple of initially unhashable args, or None if all args were hashable
unhashable_args: Option<PyTupleRef>,
parameters: PyTupleRef,
}
@@ -36,9 +40,15 @@ impl PyPayload for PyUnion {
}
impl PyUnion {
pub fn new(args: PyTupleRef, vm: &VirtualMachine) -> Self {
let parameters = make_parameters(&args, vm);
Self { args, parameters }
/// Create a new union from dedup result (internal use)
fn from_components(result: UnionComponents, vm: &VirtualMachine) -> PyResult<Self> {
let parameters = make_parameters(&result.args, vm)?;
Ok(Self {
args: result.args,
hashable_args: result.hashable_args,
unhashable_args: result.unhashable_args,
parameters,
})
}
/// Direct access to args field, matching CPython's _Py_union_args
@@ -88,10 +98,25 @@ impl PyUnion {
}
#[pyclass(
flags(BASETYPE),
flags(DISALLOW_INSTANTIATION),
with(Hashable, Comparable, AsMapping, AsNumber, Representable)
)]
impl PyUnion {
#[pygetset]
fn __name__(&self, vm: &VirtualMachine) -> PyObjectRef {
vm.ctx.new_str("Union").into()
}
#[pygetset]
fn __qualname__(&self, vm: &VirtualMachine) -> PyObjectRef {
vm.ctx.new_str("Union").into()
}
#[pygetset]
fn __origin__(&self, vm: &VirtualMachine) -> PyObjectRef {
vm.ctx.types.union_type.to_owned().into()
}
#[pygetset]
fn __parameters__(&self) -> PyObjectRef {
self.parameters.clone().into()
@@ -136,17 +161,35 @@ impl PyUnion {
}
}
fn __or__(zelf: PyObjectRef, other: PyObjectRef, vm: &VirtualMachine) -> PyObjectRef {
fn __or__(zelf: PyObjectRef, other: PyObjectRef, vm: &VirtualMachine) -> PyResult {
type_::or_(zelf, other, vm)
}
#[pymethod]
fn __mro_entries__(zelf: PyRef<Self>, _args: PyObjectRef, vm: &VirtualMachine) -> PyResult {
Err(vm.new_type_error(format!("Cannot subclass {}", zelf.repr(vm)?)))
}
#[pyclassmethod]
fn __class_getitem__(
cls: crate::builtins::PyTypeRef,
_cls: crate::builtins::PyTypeRef,
args: PyObjectRef,
vm: &VirtualMachine,
) -> PyGenericAlias {
PyGenericAlias::from_args(cls, args, vm)
) -> PyResult {
// Convert args to tuple if not already
let args_tuple = if let Some(tuple) = args.downcast_ref::<PyTuple>() {
tuple.to_owned()
} else {
PyTuple::new_ref(vec![args], &vm.ctx)
};
// Check for empty union
if args_tuple.is_empty() {
return Err(vm.new_type_error("Cannot create empty Union"));
}
// Create union using make_union to properly handle None -> NoneType conversion
make_union(&args_tuple, vm)
}
}
@@ -159,9 +202,10 @@ pub fn is_unionable(obj: PyObjectRef, vm: &VirtualMachine) -> bool {
|| obj.downcast_ref::<TypeAliasType>().is_some()
}
fn make_parameters(args: &Py<PyTuple>, vm: &VirtualMachine) -> PyTupleRef {
fn make_parameters(args: &Py<PyTuple>, vm: &VirtualMachine) -> PyResult<PyTupleRef> {
let parameters = genericalias::make_parameters(args, vm);
dedup_and_flatten_args(&parameters, vm)
let result = dedup_and_flatten_args(&parameters, vm)?;
Ok(result.args)
}
fn flatten_args(args: &Py<PyTuple>, vm: &VirtualMachine) -> PyTupleRef {
@@ -180,6 +224,12 @@ fn flatten_args(args: &Py<PyTuple>, vm: &VirtualMachine) -> PyTupleRef {
flattened_args.extend(pyref.args.iter().cloned());
} else if vm.is_none(arg) {
flattened_args.push(vm.ctx.types.none_type.to_owned().into());
} else if arg.downcast_ref::<PyStr>().is_some() {
// Convert string to ForwardRef
match string_to_forwardref(arg.clone(), vm) {
Ok(fr) => flattened_args.push(fr),
Err(_) => flattened_args.push(arg.clone()),
}
} else {
flattened_args.push(arg.clone());
};
@@ -188,31 +238,105 @@ fn flatten_args(args: &Py<PyTuple>, vm: &VirtualMachine) -> PyTupleRef {
PyTuple::new_ref(flattened_args, &vm.ctx)
}
fn dedup_and_flatten_args(args: &Py<PyTuple>, vm: &VirtualMachine) -> PyTupleRef {
fn string_to_forwardref(arg: PyObjectRef, vm: &VirtualMachine) -> PyResult {
// Import annotationlib.ForwardRef and create a ForwardRef
let annotationlib = vm.import("annotationlib", 0)?;
let forwardref_cls = annotationlib.get_attr("ForwardRef", vm)?;
forwardref_cls.call((arg,), vm)
}
/// Components for creating a PyUnion after deduplication
struct UnionComponents {
/// All unique args in order
args: PyTupleRef,
/// Frozenset of hashable args (for fast equality comparison)
hashable_args: Option<PyRef<PyFrozenSet>>,
/// Tuple of unhashable args at creation time (for hash error message)
unhashable_args: Option<PyTupleRef>,
}
fn dedup_and_flatten_args(args: &Py<PyTuple>, vm: &VirtualMachine) -> PyResult<UnionComponents> {
let args = flatten_args(args, vm);
// Use set-based deduplication like CPython:
// - For hashable elements: use Python's set semantics (hash + equality)
// - For unhashable elements: use equality comparison
//
// This avoids calling __eq__ when hashes differ, matching CPython behavior
// where `int | BadType` doesn't raise even if BadType.__eq__ raises.
let mut new_args: Vec<PyObjectRef> = Vec::with_capacity(args.len());
// Track hashable elements using a Python set (uses hash + equality)
let hashable_set = PySet::default().into_ref(&vm.ctx);
let mut hashable_list: Vec<PyObjectRef> = Vec::new();
let mut unhashable_list: Vec<PyObjectRef> = Vec::new();
for arg in &*args {
if !new_args.iter().any(|param| {
param
.rich_compare_bool(arg, PyComparisonOp::Eq, vm)
.unwrap_or_default()
}) {
new_args.push(arg.clone());
// Try to hash the element first
match arg.hash(vm) {
Ok(_) => {
// Element is hashable - use set for deduplication
// Set membership uses hash first, then equality only if hashes match
let contains = vm
.call_method(hashable_set.as_ref(), "__contains__", (arg.clone(),))
.and_then(|r| r.try_to_bool(vm))?;
if !contains {
hashable_set.add(arg.clone(), vm)?;
hashable_list.push(arg.clone());
new_args.push(arg.clone());
}
}
Err(_) => {
// Element is unhashable - use equality comparison
let mut is_duplicate = false;
for existing in &unhashable_list {
match existing.rich_compare_bool(arg, PyComparisonOp::Eq, vm) {
Ok(true) => {
is_duplicate = true;
break;
}
Ok(false) => continue,
Err(e) => return Err(e),
}
}
if !is_duplicate {
unhashable_list.push(arg.clone());
new_args.push(arg.clone());
}
}
}
}
new_args.shrink_to_fit();
PyTuple::new_ref(new_args, &vm.ctx)
// Create hashable_args frozenset if there are hashable elements
let hashable_args = if !hashable_list.is_empty() {
Some(PyFrozenSet::from_iter(vm, hashable_list.into_iter())?.into_ref(&vm.ctx))
} else {
None
};
// Create unhashable_args tuple if there are unhashable elements
let unhashable_args = if !unhashable_list.is_empty() {
Some(PyTuple::new_ref(unhashable_list, &vm.ctx))
} else {
None
};
Ok(UnionComponents {
args: PyTuple::new_ref(new_args, &vm.ctx),
hashable_args,
unhashable_args,
})
}
pub fn make_union(args: &Py<PyTuple>, vm: &VirtualMachine) -> PyObjectRef {
let args = dedup_and_flatten_args(args, vm);
match args.len() {
1 => args[0].to_owned(),
_ => PyUnion::new(args, vm).to_pyobject(vm),
}
pub fn make_union(args: &Py<PyTuple>, vm: &VirtualMachine) -> PyResult {
let result = dedup_and_flatten_args(args, vm)?;
Ok(match result.args.len() {
1 => result.args[0].to_owned(),
_ => PyUnion::from_components(result, vm)?.to_pyobject(vm),
})
}
impl PyUnion {
@@ -224,14 +348,15 @@ impl PyUnion {
needle,
vm,
)?;
let mut res;
let res;
if new_args.is_empty() {
res = make_union(&new_args, vm);
res = make_union(&new_args, vm)?;
} else {
res = new_args[0].to_owned();
let mut tmp = new_args[0].to_owned();
for arg in new_args.iter().skip(1) {
res = vm._or(&res, arg)?;
tmp = vm._or(&tmp, arg)?;
}
res = tmp;
}
Ok(res)
@@ -254,7 +379,7 @@ impl AsMapping for PyUnion {
impl AsNumber for PyUnion {
fn as_number() -> &'static PyNumberMethods {
static AS_NUMBER: PyNumberMethods = PyNumberMethods {
or: Some(|a, b, vm| PyUnion::__or__(a.to_owned(), b.to_owned(), vm).to_pyresult(vm)),
or: Some(|a, b, vm| PyUnion::__or__(a.to_owned(), b.to_owned(), vm)),
..PyNumberMethods::NOT_IMPLEMENTED
};
&AS_NUMBER
@@ -270,15 +395,62 @@ impl Comparable for PyUnion {
) -> PyResult<PyComparisonValue> {
op.eq_only(|| {
let other = class_or_notimplemented!(Self, other);
let a = PyFrozenSet::from_iter(vm, zelf.args.into_iter().cloned())?;
let b = PyFrozenSet::from_iter(vm, other.args.into_iter().cloned())?;
Ok(PyComparisonValue::Implemented(
a.into_pyobject(vm).as_object().rich_compare_bool(
b.into_pyobject(vm).as_object(),
PyComparisonOp::Eq,
vm,
)?,
))
// Check if lengths are equal
if zelf.args.len() != other.args.len() {
return Ok(PyComparisonValue::Implemented(false));
}
// Fast path: if both unions have all hashable args, compare frozensets directly
// Always use Eq here since eq_only handles Ne by negating the result
if zelf.unhashable_args.is_none()
&& other.unhashable_args.is_none()
&& let (Some(a), Some(b)) = (&zelf.hashable_args, &other.hashable_args)
{
let eq = a
.as_object()
.rich_compare_bool(b.as_object(), PyComparisonOp::Eq, vm)?;
return Ok(PyComparisonValue::Implemented(eq));
}
// Slow path: O(n^2) nested loop comparison for unhashable elements
// Check if all elements in zelf.args are in other.args
for arg_a in &*zelf.args {
let mut found = false;
for arg_b in &*other.args {
match arg_a.rich_compare_bool(arg_b, PyComparisonOp::Eq, vm) {
Ok(true) => {
found = true;
break;
}
Ok(false) => continue,
Err(e) => return Err(e), // Propagate comparison errors
}
}
if !found {
return Ok(PyComparisonValue::Implemented(false));
}
}
// Check if all elements in other.args are in zelf.args (for symmetry)
for arg_b in &*other.args {
let mut found = false;
for arg_a in &*zelf.args {
match arg_b.rich_compare_bool(arg_a, PyComparisonOp::Eq, vm) {
Ok(true) => {
found = true;
break;
}
Ok(false) => continue,
Err(e) => return Err(e), // Propagate comparison errors
}
}
if !found {
return Ok(PyComparisonValue::Implemented(false));
}
}
Ok(PyComparisonValue::Implemented(true))
})
}
}
@@ -286,7 +458,36 @@ impl Comparable for PyUnion {
impl Hashable for PyUnion {
#[inline]
fn hash(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<hash::PyHash> {
let set = PyFrozenSet::from_iter(vm, zelf.args.into_iter().cloned())?;
// If there are any unhashable args from creation time, the union is unhashable
if let Some(ref unhashable_args) = zelf.unhashable_args {
let n = unhashable_args.len();
// Try to hash each previously unhashable arg to get an error
for arg in unhashable_args.iter() {
arg.hash(vm)?;
}
// All previously unhashable args somehow became hashable
// But still raise an error to maintain consistent hashing
return Err(vm.new_type_error(format!(
"union contains {} unhashable element{}",
n,
if n > 1 { "s" } else { "" }
)));
}
// If we have a stored frozenset of hashable args, use that
if let Some(ref hashable_args) = zelf.hashable_args {
return PyFrozenSet::hash(hashable_args, vm);
}
// Fallback: compute hash from args
let mut args_to_hash = Vec::new();
for arg in &*zelf.args {
match arg.hash(vm) {
Ok(_) => args_to_hash.push(arg.clone()),
Err(e) => return Err(e),
}
}
let set = PyFrozenSet::from_iter(vm, args_to_hash.into_iter())?;
PyFrozenSet::hash(&set.into_ref(&vm.ctx), vm)
}
}

View File

@@ -149,29 +149,24 @@ mod _abc {
let items = vm.call_method(&ns, "items", ())?;
let iter = items.get_iter(vm)?;
loop {
match iter.next(vm)? {
PyIterReturn::Return(item) => {
let tuple: PyTupleRef = item
.downcast()
.map_err(|_| vm.new_type_error("items() returned non-tuple".to_owned()))?;
let elements = tuple.as_slice();
if elements.len() != 2 {
return Err(vm.new_type_error(
"items() returned item which size is not 2".to_owned(),
));
}
let key = &elements[0];
let value = &elements[1];
while let PyIterReturn::Return(item) = iter.next(vm)? {
let tuple: PyTupleRef = item
.downcast()
.map_err(|_| vm.new_type_error("items() returned non-tuple".to_owned()))?;
let elements = tuple.as_slice();
if elements.len() != 2 {
return Err(
vm.new_type_error("items() returned item which size is not 2".to_owned())
);
}
let key = &elements[0];
let value = &elements[1];
// Check if value has __isabstractmethod__ = True
if let Ok(is_abstract) = value.get_attr("__isabstractmethod__", vm) {
if is_abstract.try_to_bool(vm)? {
abstracts.push(key.clone());
}
}
}
PyIterReturn::StopIteration(_) => break,
// Check if value has __isabstractmethod__ = True
if let Ok(is_abstract) = value.get_attr("__isabstractmethod__", vm)
&& is_abstract.try_to_bool(vm)?
{
abstracts.push(key.clone());
}
}
@@ -184,25 +179,14 @@ mod _abc {
for base in bases.iter() {
if let Ok(base_abstracts) = base.get_attr("__abstractmethods__", vm) {
let iter = base_abstracts.get_iter(vm)?;
loop {
match iter.next(vm)? {
PyIterReturn::Return(key) => {
// Try to get the attribute from cls - key should be a string
if let Some(key_str) = key.downcast_ref::<PyStr>() {
if let Some(value) =
vm.get_attribute_opt(cls.to_owned(), key_str)?
{
if let Ok(is_abstract) =
value.get_attr("__isabstractmethod__", vm)
{
if is_abstract.try_to_bool(vm)? {
abstracts.push(key);
}
}
}
}
}
PyIterReturn::StopIteration(_) => break,
while let PyIterReturn::Return(key) = iter.next(vm)? {
// Try to get the attribute from cls - key should be a string
if let Some(key_str) = key.downcast_ref::<PyStr>()
&& let Some(value) = vm.get_attribute_opt(cls.to_owned(), key_str)?
&& let Ok(is_abstract) = value.get_attr("__isabstractmethod__", vm)
&& is_abstract.try_to_bool(vm)?
{
abstracts.push(key);
}
}
}
@@ -279,10 +263,10 @@ mod _abc {
let subtype: PyObjectRef = instance.class().to_owned().into();
if subtype.is(&subclass) {
let invalidation_counter = get_invalidation_counter();
if impl_data.get_cache_version() == invalidation_counter {
if in_weak_set(&impl_data.negative_cache, &subclass, vm)? {
return Ok(vm.ctx.false_value.clone().into());
}
if impl_data.get_cache_version() == invalidation_counter
&& in_weak_set(&impl_data.negative_cache, &subclass, vm)?
{
return Ok(vm.ctx.false_value.clone().into());
}
// Fall back to __subclasscheck__
return vm.call_method(&cls, "__subclasscheck__", (subclass,));
@@ -323,13 +307,12 @@ mod _abc {
let registry_copy = PyFrozenSet::from_iter(vm, registry.elements().into_iter())?;
for weak_ref_obj in registry_copy.elements() {
if let Ok(weak_ref) = weak_ref_obj.downcast::<PyWeak>() {
if let Some(rkey) = weak_ref.upgrade() {
if subclass.to_owned().is_subclass(&rkey, vm)? {
add_to_weak_set(&impl_data.cache, subclass, vm)?;
return Ok(Some(true));
}
}
if let Ok(weak_ref) = weak_ref_obj.downcast::<PyWeak>()
&& let Some(rkey) = weak_ref.upgrade()
&& subclass.to_owned().is_subclass(&rkey, vm)?
{
add_to_weak_set(&impl_data.cache, subclass, vm)?;
return Ok(Some(true));
}
}

View File

@@ -251,7 +251,7 @@ impl AsNumber for TypeVar {
static AS_NUMBER: PyNumberMethods = PyNumberMethods {
or: Some(|a, b, vm| {
let args = PyTuple::new_ref(vec![a.to_owned(), b.to_owned()], &vm.ctx);
Ok(make_union(&args, vm))
make_union(&args, vm)
}),
..PyNumberMethods::NOT_IMPLEMENTED
};
@@ -527,7 +527,7 @@ impl AsNumber for ParamSpec {
static AS_NUMBER: PyNumberMethods = PyNumberMethods {
or: Some(|a, b, vm| {
let args = PyTuple::new_ref(vec![a.to_owned(), b.to_owned()], &vm.ctx);
Ok(make_union(&args, vm))
make_union(&args, vm)
}),
..PyNumberMethods::NOT_IMPLEMENTED
};

View File

@@ -28,6 +28,7 @@ pub(crate) fn make_module(vm: &VirtualMachine) -> PyRef<PyModule> {
"ParamSpecArgs" => ParamSpecArgs::class(&vm.ctx).to_owned(),
"ParamSpecKwargs" => ParamSpecKwargs::class(&vm.ctx).to_owned(),
"Generic" => Generic::class(&vm.ctx).to_owned(),
"Union" => vm.ctx.types.union_type.to_owned(),
});
module
}
@@ -37,7 +38,6 @@ pub(crate) mod decl {
use crate::{
Py, PyObjectRef, PyPayload, PyResult, VirtualMachine,
builtins::{PyStrRef, PyTupleRef, PyType, PyTypeRef, pystr::AsPyStr, type_},
convert::ToPyResult,
function::{FuncArgs, IntoFuncArgs},
protocol::PyNumberMethods,
types::{AsNumber, Constructor, Representable},
@@ -188,7 +188,7 @@ pub(crate) mod decl {
impl AsNumber for TypeAliasType {
fn as_number() -> &'static PyNumberMethods {
static AS_NUMBER: PyNumberMethods = PyNumberMethods {
or: Some(|a, b, vm| type_::or_(a.to_owned(), b.to_owned(), vm).to_pyresult(vm)),
or: Some(|a, b, vm| type_::or_(a.to_owned(), b.to_owned(), vm)),
..PyNumberMethods::NOT_IMPLEMENTED
};
&AS_NUMBER

View File

@@ -483,7 +483,7 @@ fn hash_wrapper(zelf: &PyObject, vm: &VirtualMachine) -> PyResult<PyHash> {
/// Marks a type as unhashable. Similar to PyObject_HashNotImplemented in CPython
pub fn hash_not_implemented(zelf: &PyObject, vm: &VirtualMachine) -> PyResult<PyHash> {
Err(vm.new_type_error(format!("unhashable type: {}", zelf.class().name())))
Err(vm.new_type_error(format!("unhashable type: '{}'", zelf.class().name())))
}
fn call_wrapper(zelf: &PyObject, args: FuncArgs, vm: &VirtualMachine) -> PyResult {