Skip to main content

rustpython_vm/protocol/
object.rs

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