Skip to main content

rustpython_vm/protocol/
object.rs

1//! Object Protocol
2//! <https://docs.python.org/3/c-api/object.html>
3
4use crate::{
5    AsObject, Py, PyObject, PyObjectRef, PyRef, PyResult, TryFromObject, VirtualMachine,
6    builtins::{
7        PyBytes, PyDict, PyDictRef, PyGenericAlias, PyInt, PyList, PyStr, PyTuple, PyTupleRef,
8        PyType, PyTypeRef, PyUtf8Str, pystr::AsPyStr,
9    },
10    common::{hash::PyHash, str::to_ascii},
11    convert::{ToPyObject, ToPyResult},
12    dict_inner::DictKey,
13    function::{Either, FuncArgs, PyArithmeticValue, PySetterValue},
14    object::PyPayload,
15    protocol::PyIter,
16    types::{Constructor, PyComparisonOp},
17};
18
19// RustPython doesn't need these items
20// PyObject *Py_NotImplemented
21// Py_RETURN_NOTIMPLEMENTED
22
23impl PyObjectRef {
24    // int PyObject_Print(PyObject *o, FILE *fp, int flags)
25
26    // PyObject *PyObject_GenericGetDict(PyObject *o, void *context)
27    // int PyObject_GenericSetDict(PyObject *o, PyObject *value, void *context)
28
29    #[inline(always)]
30    pub fn rich_compare(self, other: Self, op_id: PyComparisonOp, vm: &VirtualMachine) -> PyResult {
31        self._cmp(&other, op_id, vm).map(|res| res.to_pyobject(vm))
32    }
33
34    pub fn bytes(self, vm: &VirtualMachine) -> PyResult {
35        let bytes_type = vm.ctx.types.bytes_type;
36        match self.downcast_exact::<PyInt>(vm) {
37            Ok(int) => Err(vm.new_downcast_type_error(bytes_type, &int)),
38            Err(obj) => {
39                let args = FuncArgs::from(vec![obj]);
40                <PyBytes as Constructor>::slot_new(bytes_type.to_owned(), args, vm)
41            }
42        }
43    }
44
45    // const hash_not_implemented: fn(&PyObject, &VirtualMachine) ->PyResult<PyHash> = crate::types::Unhashable::slot_hash;
46
47    pub fn is_true(self, vm: &VirtualMachine) -> PyResult<bool> {
48        self.try_to_bool(vm)
49    }
50
51    pub fn not(self, vm: &VirtualMachine) -> PyResult<bool> {
52        self.is_true(vm).map(|x| !x)
53    }
54
55    pub fn length_hint(self, defaultvalue: usize, vm: &VirtualMachine) -> PyResult<usize> {
56        Ok(vm.length_hint_opt(self)?.unwrap_or(defaultvalue))
57    }
58
59    // PyObject *PyObject_Dir(PyObject *o)
60    pub fn dir(self, vm: &VirtualMachine) -> PyResult<PyList> {
61        let attributes = self.class().get_attributes();
62
63        let dict = PyDict::from_attributes(attributes, vm)?.into_ref(&vm.ctx);
64
65        if let Some(object_dict) = self.dict() {
66            vm.call_method(
67                dict.as_object(),
68                identifier!(vm, update).as_str(),
69                (object_dict,),
70            )?;
71        }
72
73        let attributes: Vec<_> = dict.into_iter().map(|(k, _v)| k).collect();
74
75        Ok(PyList::from(attributes))
76    }
77}
78
79impl PyObject {
80    /// Takes an object and returns an iterator for it.
81    /// This is typically a new iterator but if the argument is an iterator, this
82    /// returns itself.
83    pub fn get_iter(&self, vm: &VirtualMachine) -> PyResult<PyIter> {
84        // PyObject_GetIter
85        PyIter::try_from_object(vm, self.to_owned())
86    }
87
88    // PyObject *PyObject_GetAIter(PyObject *o)
89    pub fn get_aiter(&self, vm: &VirtualMachine) -> PyResult {
90        use crate::builtins::PyCoroutine;
91
92        // Check if object has __aiter__ method
93        let aiter_method = self.class().get_attr(identifier!(vm, __aiter__));
94        let Some(_aiter_method) = aiter_method else {
95            return Err(vm.new_type_error(format!(
96                "'{}' object is not an async iterable",
97                self.class().name()
98            )));
99        };
100
101        // Call __aiter__
102        let iterator = vm.call_special_method(self, identifier!(vm, __aiter__), ())?;
103
104        // Check that __aiter__ did not return a coroutine
105        if iterator.downcast_ref::<PyCoroutine>().is_some() {
106            return Err(vm.new_type_error(
107                "'async_iterator' object cannot be interpreted as an async iterable; \
108                perhaps you forgot to call aiter()?",
109            ));
110        }
111
112        // Check that the result is an async iterator (has __anext__)
113        if !iterator.class().has_attr(identifier!(vm, __anext__)) {
114            return Err(vm.new_type_error(format!(
115                "'{}' object is not an async iterator",
116                iterator.class().name()
117            )));
118        }
119
120        Ok(iterator)
121    }
122
123    pub fn has_attr<'a>(&self, attr_name: impl AsPyStr<'a>, vm: &VirtualMachine) -> PyResult<bool> {
124        self.get_attr(attr_name, vm).map(|o| !vm.is_none(&o))
125    }
126
127    /// Get an attribute by name.
128    /// `attr_name` can be a `&str`, `String`, or `PyStrRef`.
129    pub fn get_attr<'a>(&self, attr_name: impl AsPyStr<'a>, vm: &VirtualMachine) -> PyResult {
130        let attr_name = attr_name.as_pystr(&vm.ctx);
131        self.get_attr_inner(attr_name, vm)
132    }
133
134    // get_attribute should be used for full attribute access (usually from user code).
135    #[cfg_attr(feature = "flame-it", flame("PyObjectRef"))]
136    #[inline]
137    pub(crate) fn get_attr_inner(&self, attr_name: &Py<PyStr>, vm: &VirtualMachine) -> PyResult {
138        vm_trace!("object.__getattribute__: {:?} {:?}", self, attr_name);
139        let getattro = self.class().slots.getattro.load().unwrap();
140        getattro(self, attr_name, vm).inspect_err(|exc| {
141            vm.set_attribute_error_context(exc, self.to_owned(), attr_name.to_owned());
142        })
143    }
144
145    pub fn call_set_attr(
146        &self,
147        vm: &VirtualMachine,
148        attr_name: &Py<PyStr>,
149        attr_value: PySetterValue,
150    ) -> PyResult<()> {
151        let setattro = {
152            let cls = self.class();
153            cls.slots.setattro.load().ok_or_else(|| {
154                let has_getattr = cls.slots.getattro.load().is_some();
155                vm.new_type_error(format!(
156                    "'{}' object has {} attributes ({} {})",
157                    cls.name(),
158                    if has_getattr { "only read-only" } else { "no" },
159                    if attr_value.is_assign() {
160                        "assign to"
161                    } else {
162                        "del"
163                    },
164                    attr_name
165                ))
166            })?
167        };
168        setattro(self, attr_name, attr_value, vm)
169    }
170
171    pub fn set_attr<'a>(
172        &self,
173        attr_name: impl AsPyStr<'a>,
174        attr_value: impl Into<PyObjectRef>,
175        vm: &VirtualMachine,
176    ) -> PyResult<()> {
177        let attr_name = attr_name.as_pystr(&vm.ctx);
178        let attr_value = attr_value.into();
179        self.call_set_attr(vm, attr_name, PySetterValue::Assign(attr_value))
180    }
181
182    // int PyObject_GenericSetAttr(PyObject *o, PyObject *name, PyObject *value)
183    #[cfg_attr(feature = "flame-it", flame)]
184    pub fn generic_setattr(
185        &self,
186        attr_name: &Py<PyStr>,
187        value: PySetterValue,
188        vm: &VirtualMachine,
189    ) -> PyResult<()> {
190        vm_trace!("object.__setattr__({:?}, {}, {:?})", self, attr_name, value);
191        if let Some(attr) = vm
192            .ctx
193            .interned_str(attr_name)
194            .and_then(|attr_name| self.get_class_attr(attr_name))
195        {
196            let descr_set = attr.class().slots.descr_set.load();
197            if let Some(descriptor) = descr_set {
198                return descriptor(&attr, self.to_owned(), value, vm);
199            }
200        }
201
202        if let Some(dict) = self.dict() {
203            if let PySetterValue::Assign(value) = value {
204                dict.set_item(attr_name, value, vm)?;
205            } else {
206                dict.del_item(attr_name, vm).map_err(|e| {
207                    if e.fast_isinstance(vm.ctx.exceptions.key_error) {
208                        vm.new_no_attribute_error(self.to_owned(), attr_name.to_owned())
209                    } else {
210                        e
211                    }
212                })?;
213            }
214            Ok(())
215        } else {
216            Err(vm.new_no_attribute_error(self.to_owned(), attr_name.to_owned()))
217        }
218    }
219
220    pub fn generic_getattr(&self, name: &Py<PyStr>, vm: &VirtualMachine) -> PyResult {
221        self.generic_getattr_opt(name, None, vm)?
222            .ok_or_else(|| vm.new_no_attribute_error(self.to_owned(), name.to_owned()))
223    }
224
225    /// CPython _PyObject_GenericGetAttrWithDict
226    pub fn generic_getattr_opt(
227        &self,
228        name_str: &Py<PyStr>,
229        dict: Option<PyDictRef>,
230        vm: &VirtualMachine,
231    ) -> PyResult<Option<PyObjectRef>> {
232        let name = name_str.as_wtf8();
233        let obj_cls = self.class();
234        let cls_attr_name = vm.ctx.interned_str(name_str);
235        let cls_attr = match cls_attr_name.and_then(|name| obj_cls.get_attr(name)) {
236            Some(descr) => {
237                let descr_cls = descr.class();
238                let descr_get = descr_cls.slots.descr_get.load();
239                if let Some(descr_get) = descr_get
240                    && descr_cls.slots.descr_set.load().is_some()
241                {
242                    let cls = obj_cls.to_owned().into();
243                    return descr_get(descr, Some(self.to_owned()), Some(cls), vm).map(Some);
244                }
245                Some((descr, descr_get))
246            }
247            None => None,
248        };
249
250        let dict = dict.or_else(|| self.dict());
251
252        let attr = if let Some(dict) = dict {
253            dict.get_item_opt(name, vm)?
254        } else {
255            None
256        };
257
258        if let Some(obj_attr) = attr {
259            Ok(Some(obj_attr))
260        } else if let Some((attr, descr_get)) = cls_attr {
261            match descr_get {
262                Some(descr_get) => {
263                    let cls = obj_cls.to_owned().into();
264                    descr_get(attr, Some(self.to_owned()), Some(cls), vm).map(Some)
265                }
266                None => Ok(Some(attr)),
267            }
268        } else {
269            Ok(None)
270        }
271    }
272
273    pub fn del_attr<'a>(&self, attr_name: impl AsPyStr<'a>, vm: &VirtualMachine) -> PyResult<()> {
274        let attr_name = attr_name.as_pystr(&vm.ctx);
275        self.call_set_attr(vm, attr_name, PySetterValue::Delete)
276    }
277
278    // Perform a comparison, raising TypeError when the requested comparison
279    // operator is not supported.
280    // see: PyObject_RichCompare / do_richcompare
281    #[inline] // called by ExecutingFrame::execute_compare with const op
282    fn _cmp(
283        &self,
284        other: &Self,
285        op: PyComparisonOp,
286        vm: &VirtualMachine,
287    ) -> PyResult<Either<PyObjectRef, bool>> {
288        // Single recursion guard for the entire comparison
289        // (do_richcompare in Objects/object.c).
290        vm.with_recursion("in comparison", || self._cmp_inner(other, op, vm))
291    }
292
293    fn _cmp_inner(
294        &self,
295        other: &Self,
296        op: PyComparisonOp,
297        vm: &VirtualMachine,
298    ) -> PyResult<Either<PyObjectRef, bool>> {
299        let swapped = op.swapped();
300        let call_cmp = |obj: &Self, other: &Self, op| {
301            let Some(cmp) = obj.class().slots.richcompare.load() else {
302                return Ok(PyArithmeticValue::NotImplemented);
303            };
304            let r = match cmp(obj, other, op, vm)? {
305                Either::A(obj) => PyArithmeticValue::from_object(vm, obj).map(Either::A),
306                Either::B(arithmetic) => arithmetic.map(Either::B),
307            };
308            Ok(r)
309        };
310
311        let mut checked_reverse_op = false;
312        let is_strict_subclass = {
313            let self_class = self.class();
314            let other_class = other.class();
315            !self_class.is(other_class) && other_class.fast_issubclass(self_class)
316        };
317        if is_strict_subclass {
318            let res = call_cmp(other, self, swapped)?;
319            checked_reverse_op = true;
320            if let PyArithmeticValue::Implemented(x) = res {
321                return Ok(x);
322            }
323        }
324        if let PyArithmeticValue::Implemented(x) = call_cmp(self, other, op)? {
325            return Ok(x);
326        }
327        if !checked_reverse_op {
328            let res = call_cmp(other, self, swapped)?;
329            if let PyArithmeticValue::Implemented(x) = res {
330                return Ok(x);
331            }
332        }
333        match op {
334            PyComparisonOp::Eq => Ok(Either::B(self.is(&other))),
335            PyComparisonOp::Ne => Ok(Either::B(!self.is(&other))),
336            _ => Err(vm.new_type_error(format!(
337                "'{}' not supported between instances of '{}' and '{}'",
338                op.operator_token(),
339                self.class().name(),
340                other.class().name()
341            ))),
342        }
343    }
344    #[inline(always)]
345    pub fn rich_compare_bool(
346        &self,
347        other: &Self,
348        op_id: PyComparisonOp,
349        vm: &VirtualMachine,
350    ) -> PyResult<bool> {
351        match self._cmp(other, op_id, vm)? {
352            Either::A(obj) => obj.try_to_bool(vm),
353            Either::B(other) => Ok(other),
354        }
355    }
356
357    pub fn repr_utf8(&self, vm: &VirtualMachine) -> PyResult<PyRef<PyUtf8Str>> {
358        self.repr(vm)?.try_into_utf8(vm)
359    }
360
361    pub fn repr(&self, vm: &VirtualMachine) -> PyResult<PyRef<PyStr>> {
362        vm.with_recursion("while getting the repr of an object", || {
363            self.class().slots.repr.load().map_or_else(
364                || {
365                    Err(vm.new_runtime_error(format!(
366                    "BUG: object of type '{}' has no __repr__ method. This is a bug in RustPython.",
367                    self.class().name()
368                )))
369                },
370                |repr| repr(self, vm),
371            )
372        })
373    }
374
375    pub fn ascii(&self, vm: &VirtualMachine) -> PyResult<PyRef<PyStr>> {
376        let repr = self.repr(vm)?;
377        if repr.as_wtf8().is_ascii() {
378            Ok(repr)
379        } else {
380            Ok(vm.ctx.new_str(to_ascii(repr.as_wtf8())))
381        }
382    }
383
384    pub fn str_utf8(&self, vm: &VirtualMachine) -> PyResult<PyRef<PyUtf8Str>> {
385        self.str(vm)?.try_into_utf8(vm)
386    }
387    pub fn str(&self, vm: &VirtualMachine) -> PyResult<PyRef<PyStr>> {
388        let obj = match self.to_owned().downcast_exact::<PyStr>(vm) {
389            Ok(s) => return Ok(s.into_pyref()),
390            Err(obj) => obj,
391        };
392        // Fast path for exact int: skip __str__ method resolution
393        let obj = match obj.downcast_exact::<PyInt>(vm) {
394            Ok(int) => {
395                return Ok(vm.ctx.new_str(int.to_str_radix_10()));
396            }
397            Err(obj) => obj,
398        };
399        // TODO: replace to obj.class().slots.str
400        let str_method = match vm.get_special_method(&obj, identifier!(vm, __str__))? {
401            Some(str_method) => str_method,
402            None => return obj.repr(vm),
403        };
404        let s = str_method.invoke((), vm)?;
405        s.downcast::<PyStr>().map_err(|obj| {
406            vm.new_type_error(format!(
407                "__str__ returned non-string (type {})",
408                obj.class().name()
409            ))
410        })
411    }
412
413    // Equivalent to CPython's check_class. Returns Ok(()) if cls is a valid class,
414    // Err with TypeError if not. Uses abstract_get_bases internally.
415    fn check_class<F>(&self, vm: &VirtualMachine, msg: F) -> PyResult<()>
416    where
417        F: Fn() -> String,
418    {
419        let cls = self;
420        match cls.abstract_get_bases(vm)? {
421            Some(_bases) => Ok(()), // Has __bases__, it's a valid class
422            None => {
423                // No __bases__ or __bases__ is not a tuple
424                Err(vm.new_type_error(msg()))
425            }
426        }
427    }
428
429    /// abstract_get_bases() has logically 4 return states:
430    /// 1. getattr(cls, '__bases__') could raise an AttributeError
431    /// 2. getattr(cls, '__bases__') could raise some other exception
432    /// 3. getattr(cls, '__bases__') could return a tuple
433    /// 4. getattr(cls, '__bases__') could return something other than a tuple
434    ///
435    /// Only state #3 returns Some(tuple). AttributeErrors are masked by returning None.
436    /// If an object other than a tuple comes out of __bases__, then again, None is returned.
437    /// Other exceptions are propagated.
438    fn abstract_get_bases(&self, vm: &VirtualMachine) -> PyResult<Option<PyTupleRef>> {
439        match vm.get_attribute_opt(self.to_owned(), identifier!(vm, __bases__))? {
440            Some(bases) => {
441                // Check if it's a tuple
442                match PyTupleRef::try_from_object(vm, bases) {
443                    Ok(tuple) => Ok(Some(tuple)),
444                    Err(_) => Ok(None), // Not a tuple, return None
445                }
446            }
447            None => Ok(None), // AttributeError was masked
448        }
449    }
450
451    fn abstract_issubclass(&self, cls: &Self, vm: &VirtualMachine) -> PyResult<bool> {
452        // Store the current derived class to check
453        let mut bases: PyTupleRef;
454        let mut derived = self;
455
456        // First loop: handle single inheritance without recursion
457        let bases = loop {
458            if derived.is(cls) {
459                return Ok(true);
460            }
461
462            let Some(derived_bases) = derived.abstract_get_bases(vm)? else {
463                return Ok(false);
464            };
465
466            let n = derived_bases.len();
467            match n {
468                0 => return Ok(false),
469                1 => {
470                    // Avoid recursion in the single inheritance case
471                    // Get the next derived class and continue the loop
472                    bases = derived_bases;
473                    derived = &bases.as_slice()[0];
474                    continue;
475                }
476                _ => {
477                    // Multiple inheritance - handle recursively
478                    break derived_bases;
479                }
480            }
481        };
482
483        let n = bases.len();
484        // At this point we know n >= 2
485        debug_assert!(n >= 2);
486
487        for i in 0..n {
488            let result = vm.with_recursion("in __issubclass__", || {
489                bases.as_slice()[i].abstract_issubclass(cls, vm)
490            })?;
491            if result {
492                return Ok(true);
493            }
494        }
495
496        Ok(false)
497    }
498
499    fn recursive_issubclass(&self, cls: &Self, vm: &VirtualMachine) -> PyResult<bool> {
500        // Fast path for both being types (matches CPython's PyType_Check)
501        if let Some(cls) = PyType::check(cls)
502            && let Some(derived) = PyType::check(self)
503        {
504            // PyType_IsSubtype equivalent
505            return Ok(derived.is_subtype(cls));
506        }
507        // Check if derived is a class
508        self.check_class(vm, || {
509            format!("issubclass() arg 1 must be a class, not {}", self.class())
510        })?;
511
512        // Check if cls is a class, tuple, or union (matches CPython's order and message)
513        if !cls.class().is(vm.ctx.types.union_type) {
514            cls.check_class(vm, || {
515                format!(
516                    "issubclass() arg 2 must be a class, a tuple of classes, or a union, not {}",
517                    cls.class()
518                )
519            })?;
520        }
521
522        self.abstract_issubclass(cls, vm)
523    }
524
525    /// Real issubclass check without going through __subclasscheck__
526    /// This is equivalent to CPython's _PyObject_RealIsSubclass which just calls recursive_issubclass
527    pub fn real_is_subclass(&self, cls: &Self, vm: &VirtualMachine) -> PyResult<bool> {
528        self.recursive_issubclass(cls, vm)
529    }
530
531    /// Determines if `self` is a subclass of `cls`, either directly, indirectly or virtually
532    /// via the __subclasscheck__ magic method.
533    /// PyObject_IsSubclass/object_issubclass
534    pub fn is_subclass(&self, cls: &Self, vm: &VirtualMachine) -> PyResult<bool> {
535        let derived = self;
536        // PyType_CheckExact(cls)
537        if cls.class().is(vm.ctx.types.type_type) {
538            if derived.is(cls) {
539                return Ok(true);
540            }
541            return derived.recursive_issubclass(cls, vm);
542        }
543
544        // Check for Union type - CPython handles this before tuple
545        let cls = if cls.class().is(vm.ctx.types.union_type) {
546            // Get the __args__ attribute which contains the union members
547            // Match CPython's _Py_union_args which directly accesses the args field
548            let union = cls
549                .downcast_ref::<crate::builtins::PyUnion>()
550                .expect("union is already checked");
551            union.args().as_object()
552        } else {
553            cls
554        };
555
556        // Check if cls is a tuple
557        if let Some(tuple) = cls.downcast_ref::<PyTuple>() {
558            for item in tuple {
559                if vm.with_recursion("in __subclasscheck__", || derived.is_subclass(item, vm))? {
560                    return Ok(true);
561                }
562            }
563            return Ok(false);
564        }
565
566        // Check for __subclasscheck__ method using lookup_special (matches CPython)
567        if let Some(checker) = cls.lookup_special(identifier!(vm, __subclasscheck__), vm) {
568            let res = vm.with_recursion("in __subclasscheck__", || {
569                checker.call((derived.to_owned(),), vm)
570            })?;
571            return res.try_to_bool(vm);
572        }
573
574        derived.recursive_issubclass(cls, vm)
575    }
576
577    // _PyObject_RealIsInstance
578    pub(crate) fn real_is_instance(&self, cls: &Self, vm: &VirtualMachine) -> PyResult<bool> {
579        self.object_isinstance(cls, vm)
580    }
581
582    /// Real isinstance check without going through __instancecheck__
583    /// This is equivalent to CPython's _PyObject_RealIsInstance/object_isinstance
584    fn object_isinstance(&self, cls: &Self, vm: &VirtualMachine) -> PyResult<bool> {
585        if let Ok(cls) = cls.try_to_ref::<PyType>(vm) {
586            // PyType_Check(cls) - cls is a type object
587            let mut retval = self.class().is_subtype(cls);
588            if !retval
589                && let Some(i_cls) =
590                    vm.get_attribute_opt(self.to_owned(), identifier!(vm, __class__))?
591                && let Ok(i_cls_type) = PyTypeRef::try_from_object(vm, i_cls)
592                && !i_cls_type.is(self.class())
593            {
594                retval = i_cls_type.is_subtype(cls);
595            }
596            Ok(retval)
597        } else {
598            // Not a type object, check if it's a valid class
599            cls.check_class(vm, || {
600                format!(
601                    "isinstance() arg 2 must be a type, a tuple of types, or a union, not {}",
602                    cls.class()
603                )
604            })?;
605
606            if let Some(i_cls) =
607                vm.get_attribute_opt(self.to_owned(), identifier!(vm, __class__))?
608            {
609                i_cls.abstract_issubclass(cls, vm)
610            } else {
611                Ok(false)
612            }
613        }
614    }
615
616    /// Determines if `self` is an instance of `cls`, either directly, indirectly or virtually via
617    /// the __instancecheck__ magic method.
618    pub fn is_instance(&self, cls: &Self, vm: &VirtualMachine) -> PyResult<bool> {
619        self.object_recursive_isinstance(cls, vm)
620    }
621
622    // This is object_recursive_isinstance from CPython's Objects/abstract.c
623    fn object_recursive_isinstance(&self, cls: &Self, vm: &VirtualMachine) -> PyResult<bool> {
624        // PyObject_TypeCheck(inst, (PyTypeObject *)cls)
625        // This is an exact check of the type
626        if self.class().is(cls) {
627            return Ok(true);
628        }
629
630        // PyType_CheckExact(cls) optimization
631        if cls.class().is(vm.ctx.types.type_type) {
632            // When cls is exactly a type (not a subclass), use object_isinstance
633            // to avoid going through __instancecheck__ (matches CPython behavior)
634            return self.object_isinstance(cls, vm);
635        }
636
637        // Check for Union type (e.g., int | str) - CPython checks this before tuple
638        let cls = if cls.class().is(vm.ctx.types.union_type) {
639            // Match CPython's _Py_union_args which directly accesses the args field
640            let union = cls
641                .try_to_ref::<crate::builtins::PyUnion>(vm)
642                .expect("checked by is");
643            union.args().as_object()
644        } else {
645            cls
646        };
647
648        // Check if cls is a tuple
649        if let Some(tuple) = cls.downcast_ref::<PyTuple>() {
650            for item in tuple {
651                if vm.with_recursion("in __instancecheck__", || {
652                    self.object_recursive_isinstance(item, vm)
653                })? {
654                    return Ok(true);
655                }
656            }
657            return Ok(false);
658        }
659
660        // Check for __instancecheck__ method using lookup_special (matches CPython)
661        if let Some(checker) = cls.lookup_special(identifier!(vm, __instancecheck__), vm) {
662            let res = vm.with_recursion("in __instancecheck__", || {
663                checker.call((self.to_owned(),), vm)
664            })?;
665            return res.try_to_bool(vm);
666        }
667
668        // Fall back to object_isinstance (without going through __instancecheck__ again)
669        self.object_isinstance(cls, vm)
670    }
671
672    pub fn hash(&self, vm: &VirtualMachine) -> PyResult<PyHash> {
673        if let Some(hash) = self.class().slots.hash.load() {
674            return hash(self, vm);
675        }
676
677        Err(vm.new_exception_msg(
678            vm.ctx.exceptions.type_error.to_owned(),
679            format!("unhashable type: '{}'", self.class().name()).into(),
680        ))
681    }
682
683    // type protocol
684    // PyObject *PyObject_Type(PyObject *o)
685    pub fn obj_type(&self) -> PyObjectRef {
686        self.class().to_owned().into()
687    }
688
689    // int PyObject_TypeCheck(PyObject *o, PyTypeObject *type)
690    pub fn type_check(&self, typ: &Py<PyType>) -> bool {
691        self.fast_isinstance(typ)
692    }
693
694    pub fn length_opt(&self, vm: &VirtualMachine) -> Option<PyResult<usize>> {
695        self.sequence_unchecked()
696            .length_opt(vm)
697            .or_else(|| self.mapping_unchecked().length_opt(vm))
698    }
699
700    pub fn length(&self, vm: &VirtualMachine) -> PyResult<usize> {
701        self.length_opt(vm).ok_or_else(|| {
702            vm.new_type_error(format!(
703                "object of type '{}' has no len()",
704                self.class().name()
705            ))
706        })?
707    }
708
709    pub fn get_item<K: DictKey + ?Sized>(&self, needle: &K, vm: &VirtualMachine) -> PyResult {
710        if let Some(dict) = self.downcast_ref_if_exact::<PyDict>(vm) {
711            return dict.get_item(needle, vm);
712        }
713
714        let needle = needle.to_pyobject(vm);
715
716        if let Ok(mapping) = self.try_mapping(vm) {
717            mapping.subscript(&needle, vm)
718        } else if let Ok(seq) = self.try_sequence(vm) {
719            let i = needle.key_as_isize(vm)?;
720            seq.get_item(i, vm)
721        } else {
722            if self.class().fast_issubclass(vm.ctx.types.type_type) {
723                if self.is(vm.ctx.types.type_type) {
724                    return PyGenericAlias::from_args(self.class().to_owned(), needle, vm)
725                        .to_pyresult(vm);
726                }
727
728                if let Some(class_getitem) =
729                    vm.get_attribute_opt(self.to_owned(), identifier!(vm, __class_getitem__))?
730                    && !vm.is_none(&class_getitem)
731                {
732                    return class_getitem.call((needle,), vm);
733                }
734                return Err(vm.new_type_error(format!(
735                    "type '{}' is not subscriptable",
736                    self.downcast_ref::<PyType>().unwrap().name()
737                )));
738            }
739            Err(vm.new_type_error(format!("'{}' object is not subscriptable", self.class())))
740        }
741    }
742
743    pub fn set_item<K: DictKey + ?Sized>(
744        &self,
745        needle: &K,
746        value: PyObjectRef,
747        vm: &VirtualMachine,
748    ) -> PyResult<()> {
749        if let Some(dict) = self.downcast_ref_if_exact::<PyDict>(vm) {
750            return dict.set_item(needle, value, vm);
751        }
752
753        let mapping = self.mapping_unchecked();
754        if let Some(f) = mapping.slots().ass_subscript.load() {
755            let needle = needle.to_pyobject(vm);
756            return f(mapping, &needle, Some(value), vm);
757        }
758
759        let seq = self.sequence_unchecked();
760        if let Some(f) = seq.slots().ass_item.load() {
761            let i = needle.key_as_isize(vm)?;
762            return f(seq, i, Some(value), vm);
763        }
764
765        Err(vm.new_type_error(format!(
766            "'{}' does not support item assignment",
767            self.class()
768        )))
769    }
770
771    pub fn del_item<K: DictKey + ?Sized>(&self, needle: &K, vm: &VirtualMachine) -> PyResult<()> {
772        if let Some(dict) = self.downcast_ref_if_exact::<PyDict>(vm) {
773            return dict.del_item(needle, vm);
774        }
775
776        let mapping = self.mapping_unchecked();
777        if let Some(f) = mapping.slots().ass_subscript.load() {
778            let needle = needle.to_pyobject(vm);
779            return f(mapping, &needle, None, vm);
780        }
781        let seq = self.sequence_unchecked();
782        if let Some(f) = seq.slots().ass_item.load() {
783            let i = needle.key_as_isize(vm)?;
784            return f(seq, i, None, vm);
785        }
786
787        Err(vm.new_type_error(format!("'{}' does not support item deletion", self.class())))
788    }
789
790    /// Equivalent to CPython's _PyObject_LookupSpecial
791    /// Looks up a special method in the type's MRO without checking instance dict.
792    /// Returns None if not found (masking AttributeError like CPython).
793    pub fn lookup_special(&self, attr: &Py<PyStr>, vm: &VirtualMachine) -> Option<PyObjectRef> {
794        let obj_cls = self.class();
795
796        // Use PyType::lookup_ref (equivalent to CPython's _PyType_LookupRef)
797        let res = obj_cls.lookup_ref(attr, vm)?;
798
799        // If it's a descriptor, call its __get__ method
800        let descr_get = res.class().slots.descr_get.load();
801        if let Some(descr_get) = descr_get {
802            let obj_cls = obj_cls.to_owned().into();
803            // CPython ignores exceptions in _PyObject_LookupSpecial and returns NULL
804            descr_get(res, Some(self.to_owned()), Some(obj_cls), vm).ok()
805        } else {
806            Some(res)
807        }
808    }
809}