Skip to main content

rustpython_vm/builtins/
object.rs

1use super::{PyDict, PyDictRef, PyList, PyStr, PyStrRef, PyType, PyTypeRef, PyUtf8StrRef};
2use crate::common::hash::PyHash;
3use crate::types::PyTypeFlags;
4use crate::{
5    AsObject, Context, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine,
6    class::PyClassImpl,
7    convert::ToPyResult,
8    function::{
9        ArgumentError, Either, FromArgs, FuncArgs, Param, PyArithmeticValue, PyComparisonValue,
10        PySetterValue,
11    },
12    types::{Constructor, Initializer, PyComparisonOp},
13};
14use itertools::Itertools;
15
16#[pyclass(module = false, name = "object")]
17#[derive(Debug)]
18pub struct PyBaseObject;
19
20impl PyPayload for PyBaseObject {
21    #[inline]
22    fn class(ctx: &Context) -> &'static Py<PyType> {
23        ctx.types.object_type
24    }
25}
26
27pub struct ObjectArgs;
28
29impl FromArgs for ObjectArgs {
30    const PARAMS: Option<&'static [Param]> = Some(&[]);
31
32    fn from_args(_vm: &VirtualMachine, args: &mut FuncArgs) -> Result<Self, ArgumentError> {
33        core::mem::take(args);
34        Ok(Self)
35    }
36}
37
38impl Constructor for PyBaseObject {
39    type Args = ObjectArgs;
40
41    // = object_new
42    fn slot_new(cls: PyTypeRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult {
43        if !args.args.is_empty() || !args.kwargs.is_empty() {
44            // Check if type's __new__ != object.__new__
45            let tp_new = cls.get_attr(identifier!(vm, __new__));
46            let object_new = vm.ctx.types.object_type.get_attr(identifier!(vm, __new__));
47
48            if let (Some(tp_new), Some(object_new)) = (tp_new, object_new) {
49                if !tp_new.is(&object_new) {
50                    // Type has its own __new__, so object.__new__ is being called
51                    // with excess args. This is the first error case in CPython
52                    return Err(vm.new_type_error(
53                        "object.__new__() takes exactly one argument (the type to instantiate)",
54                    ));
55                }
56
57                // If we reach here, tp_new == object_new
58                // Now check if type's __init__ == object.__init__
59                let tp_init = cls.get_attr(identifier!(vm, __init__));
60                let object_init = vm.ctx.types.object_type.get_attr(identifier!(vm, __init__));
61
62                if let (Some(tp_init), Some(object_init)) = (tp_init, object_init)
63                    && tp_init.is(&object_init)
64                {
65                    // Both __new__ and __init__ are object's versions,
66                    // so the type accepts no arguments
67                    return Err(vm.new_type_error(format!("{}() takes no arguments", cls.name())));
68                }
69                // If tp_init != object_init, then the type has custom __init__
70                // which might accept arguments, so we allow it
71            }
72        }
73
74        // Ensure that all abstract methods are implemented before instantiating instance.
75        if let Some(abs_methods) = cls.get_attr(identifier!(vm, __abstractmethods__)) {
76            let methods: Vec<PyUtf8StrRef> = abs_methods.try_to_value(vm)?;
77            let unimplemented_abstract_method_count = methods.len();
78            if unimplemented_abstract_method_count > 0 {
79                let methods: String = Itertools::intersperse(
80                    methods.iter().map(|name| name.as_str().to_owned()),
81                    "', '".to_owned(),
82                )
83                .collect();
84                let name = cls.name().to_string();
85                let noun = if unimplemented_abstract_method_count == 1 {
86                    "method"
87                } else {
88                    "methods"
89                };
90                return Err(vm.new_type_error(format!(
91                    "class {name} without an implementation for abstract {noun} '{methods}'"
92                )));
93            }
94        }
95
96        generic_alloc(cls, 0, vm)
97    }
98
99    fn py_new(_cls: &Py<PyType>, _args: Self::Args, _vm: &VirtualMachine) -> PyResult<Self> {
100        unimplemented!("use slot_new")
101    }
102}
103
104#[expect(clippy::unnecessary_wraps, reason = "Needs to comply with a signature")]
105pub(crate) fn generic_alloc(cls: PyTypeRef, _nitems: usize, vm: &VirtualMachine) -> PyResult {
106    // Only create dict if the class has HAS_DICT flag (i.e., __slots__ was not defined
107    // or __dict__ is in __slots__)
108    let dict = if cls
109        .slots
110        .flags
111        .has_feature(crate::types::PyTypeFlags::HAS_DICT)
112    {
113        Some(vm.ctx.new_dict())
114    } else {
115        None
116    };
117    Ok(crate::PyRef::new_ref(PyBaseObject, cls, dict).into())
118}
119
120impl Initializer for PyBaseObject {
121    type Args = ObjectArgs;
122
123    // object_init: excess_args validation
124    fn slot_init(zelf: &PyObject, args: FuncArgs, vm: &VirtualMachine) -> PyResult<()> {
125        if args.is_empty() {
126            return Ok(());
127        }
128
129        let typ = zelf.class();
130        let object_type = &vm.ctx.types.object_type;
131
132        let typ_init = typ.slots.init.load().map(|f| crate::types::fn_addr(f));
133        let object_init = object_type
134            .slots
135            .init
136            .load()
137            .map(|f| crate::types::fn_addr(f));
138
139        // if (type->tp_init != object_init) → first error
140        if typ_init != object_init {
141            return Err(vm.new_type_error(
142                "object.__init__() takes exactly one argument (the instance to initialize)",
143            ));
144        }
145
146        // if (type->tp_new == object_new) → second error
147        if let (Some(typ_new), Some(object_new)) = (
148            typ.get_attr(identifier!(vm, __new__)),
149            object_type.get_attr(identifier!(vm, __new__)),
150        ) && typ_new.is(&object_new)
151        {
152            return Err(vm.new_type_error(format!(
153                "{}.__init__() takes exactly one argument (the instance to initialize)",
154                typ.name()
155            )));
156        }
157
158        // Both conditions false → OK (e.g., tuple, dict with custom __new__)
159        Ok(())
160    }
161
162    fn init(_zelf: &Py<Self>, _args: Self::Args, _vm: &VirtualMachine) -> PyResult<()> {
163        unreachable!("slot_init is defined")
164    }
165}
166
167// _PyType_GetSlotNames
168fn type_slot_names(typ: &Py<PyType>, vm: &VirtualMachine) -> PyResult<Option<super::PyListRef>> {
169    // The class caches its slot names in `__slotnames__`.
170    if let Some(slot_names) = typ.get_direct_attr(identifier!(vm.ctx, __slotnames__)) {
171        return match_class!(match slot_names {
172            l @ super::PyList => Ok(Some(l)),
173            _n @ super::PyNone => Ok(None),
174            other => Err(vm.new_type_error(format!(
175                "{:.200}.__slotnames__ should be a list or None, not {:.200}",
176                typ.name(),
177                other.class().name()
178            ))),
179        });
180    }
181
182    // copyreg._slotnames collects the slots of the class and its bases and
183    // caches them in `__slotnames__`.
184    let copyreg = vm.import("copyreg", 0)?;
185    let copyreg_slotnames = copyreg.get_attr("_slotnames", vm)?;
186    let slot_names = copyreg_slotnames.call((typ.to_owned(),), vm)?;
187    let result = match_class!(match slot_names {
188        l @ super::PyList => Some(l),
189        _n @ super::PyNone => None,
190        _ => return Err(vm.new_type_error("copyreg._slotnames didn't return a list or None")),
191    });
192    Ok(result)
193}
194
195// object_getstate_default
196fn object_getstate_default(obj: &PyObject, required: bool, vm: &VirtualMachine) -> PyResult {
197    // Check itemsize
198    if required && obj.class().slots().itemsize > 0 {
199        return Err(vm.new_type_error(format!("cannot pickle {:.200} objects", obj.class().name())));
200    }
201
202    let state = if obj.dict().is_none_or(|d| d.is_empty()) {
203        vm.ctx.none()
204    } else {
205        // let state = object_get_dict(obj.clone(), obj.ctx()).unwrap();
206        let Some(state) = obj.dict() else {
207            return Ok(vm.ctx.none());
208        };
209        state.into()
210    };
211
212    let slot_names = type_slot_names(obj.class(), vm)?;
213
214    if required {
215        // Dict, weakref list, and slot cells sit in the prefix in front of
216        // the object header. `slots.basicsize` is the header plus the payload,
217        // so only state stored inside the payload counts.
218        let basicsize = vm.ctx.types.object_type.slots().basicsize;
219        if obj.class().slots().basicsize > basicsize {
220            return Err(vm.new_type_error(format!("cannot pickle '{}' object", obj.class().name())));
221        }
222    }
223
224    if let Some(slot_names) = slot_names {
225        let slot_names_len = slot_names.__len__();
226        if slot_names_len > 0 {
227            let slots = vm.ctx.new_dict();
228            for i in 0..slot_names_len {
229                let borrowed_names = slot_names.borrow_vec();
230                // Check if slotnames changed during iteration
231                if borrowed_names.len() != slot_names_len {
232                    return Err(vm.new_runtime_error("__slotnames__ changed size during iteration"));
233                }
234                let name = borrowed_names[i].downcast_ref::<PyStr>().unwrap();
235                let Ok(value) = obj.get_attr(name, vm) else {
236                    continue;
237                };
238                slots.set_item(name.as_wtf8(), value, vm).unwrap();
239            }
240
241            if !slots.is_empty() {
242                return (state, slots).to_pyresult(vm);
243            }
244        }
245    }
246
247    Ok(state)
248}
249
250// object_getstate
251// fn object_getstate(
252//     obj: &PyObject,
253//     required: bool,
254//     vm: &VirtualMachine,
255// ) -> PyResult {
256//     let getstate = obj.get_attr(identifier!(vm, __getstate__), vm)?;
257//     if vm.is_none(&getstate) {
258//         return Ok(None);
259//     }
260
261//     let getstate = match getstate.downcast_exact::<PyNativeFunction>(vm) {
262//         Ok(getstate)
263//             if getstate
264//                 .get_self()
265//                 .map_or(false, |self_obj| self_obj.is(obj))
266//                 && std::ptr::addr_eq(
267//                     getstate.as_func() as *const _,
268//                     &PyBaseObject::__getstate__ as &dyn crate::function::PyNativeFn as *const _,
269//                 ) =>
270//         {
271//             return object_getstate_default(obj, required, vm);
272//         }
273//         Ok(getstate) => getstate.into_pyref().into(),
274//         Err(getstate) => getstate,
275//     };
276//     getstate.call((), vm)
277// }
278
279#[pyclass(with(Constructor, Initializer), flags(BASETYPE))]
280impl PyBaseObject {
281    #[pymethod]
282    fn __getstate__(zelf: PyObjectRef, vm: &VirtualMachine) -> PyResult {
283        object_getstate_default(&zelf, false, vm)
284    }
285
286    #[pyslot]
287    fn slot_richcompare(
288        zelf: &PyObject,
289        other: &PyObject,
290        op: PyComparisonOp,
291        vm: &VirtualMachine,
292    ) -> PyResult<Either<PyObjectRef, PyComparisonValue>> {
293        Self::cmp(zelf, other, op, vm).map(Either::B)
294    }
295
296    #[inline(always)]
297    fn cmp(
298        zelf: &PyObject,
299        other: &PyObject,
300        op: PyComparisonOp,
301        vm: &VirtualMachine,
302    ) -> PyResult<PyComparisonValue> {
303        let res = match op {
304            PyComparisonOp::Eq => {
305                if zelf.is(other) {
306                    PyComparisonValue::Implemented(true)
307                } else {
308                    PyComparisonValue::NotImplemented
309                }
310            }
311            PyComparisonOp::Ne => {
312                let cmp = zelf.class().slots().richcompare.load().unwrap();
313                let value = match cmp(zelf, other, PyComparisonOp::Eq, vm)? {
314                    Either::A(obj) => PyArithmeticValue::from_object(vm, obj)
315                        .map(|obj| obj.try_to_bool(vm))
316                        .transpose()?,
317                    Either::B(value) => value,
318                };
319                value.map(|v| !v)
320            }
321            _ => PyComparisonValue::NotImplemented,
322        };
323        Ok(res)
324    }
325
326    // __setattr__ and __delattr__ are added as slot wrappers by add_operators.
327    #[pyslot]
328    pub(crate) fn slot_setattro(
329        obj: &PyObject,
330        attr_name: &Py<PyStr>,
331        value: PySetterValue,
332        vm: &VirtualMachine,
333    ) -> PyResult<()> {
334        obj.generic_setattr(attr_name, value, vm)
335    }
336
337    // Return str(self).
338    #[pyslot]
339    fn slot_str(zelf: &PyObject, vm: &VirtualMachine) -> PyResult<PyStrRef> {
340        // FIXME: try tp_repr first and fallback to object.__repr__
341        zelf.repr(vm)
342    }
343
344    #[pyslot]
345    fn slot_repr(zelf: &PyObject, vm: &VirtualMachine) -> PyResult<PyStrRef> {
346        let class = zelf.class();
347        // A missing `__module__` is not an error here.
348        let module = class.__module__(vm).ok();
349        match (
350            class
351                .__qualname__(vm)
352                .downcast_ref::<PyStr>()
353                .map(|n| n.as_wtf8()),
354            module
355                .as_ref()
356                .and_then(|m| m.downcast_ref::<PyStr>())
357                .map(|m| m.as_wtf8()),
358        ) {
359            (None, _) => Err(vm.new_type_error("Unknown qualified name")),
360            (Some(qualname), Some(module)) if module != "builtins" => Ok(PyStr::from(format!(
361                "<{}.{} object at {:#x}>",
362                module,
363                qualname,
364                zelf.get_id()
365            ))
366            .into_ref(&vm.ctx)),
367            _ => Ok(PyStr::from(format!(
368                "<{} object at {:#x}>",
369                class.slot_name(),
370                zelf.get_id()
371            ))
372            .into_ref(&vm.ctx)),
373        }
374    }
375
376    #[pyclassmethod]
377    fn __subclasshook__(_cls: PyTypeRef, _object: PyObjectRef, vm: &VirtualMachine) -> PyObjectRef {
378        vm.ctx.not_implemented()
379    }
380
381    #[pyclassmethod]
382    fn __init_subclass__(_cls: PyTypeRef) {}
383
384    #[pymethod]
385    pub fn __dir__(zelf: PyObjectRef, vm: &VirtualMachine) -> PyResult<PyList> {
386        let mut names: Vec<PyObjectRef> = Vec::new();
387
388        match zelf.get_attr(identifier!(vm, __dict__), vm) {
389            Ok(obj) => {
390                if let Ok(dict) = obj.downcast::<PyDict>() {
391                    names.extend(
392                        dict.into_iter()
393                            .filter_map(|(k, _)| k.downcast_ref::<PyStr>().is_some().then_some(k)),
394                    );
395                }
396            }
397            Err(e) if e.fast_isinstance(vm.ctx.exceptions.attribute_error) => {}
398            Err(e) => return Err(e),
399        }
400
401        match zelf.get_attr(identifier!(vm, __class__), vm) {
402            Ok(cls_obj) => {
403                if let Some(cls) = cls_obj.downcast_ref::<PyType>() {
404                    for (name, _) in cls.get_attributes(&vm.ctx) {
405                        names.push(name.to_object());
406                    }
407                }
408            }
409            Err(e) if e.fast_isinstance(vm.ctx.exceptions.attribute_error) => {}
410            Err(e) => return Err(e),
411        }
412
413        let lst = PyList::from(names);
414        lst.sort(Default::default(), vm)?;
415        Ok(lst)
416    }
417
418    #[pymethod]
419    fn __format__(
420        obj: PyObjectRef,
421        format_spec: PyStrRef,
422        vm: &VirtualMachine,
423    ) -> PyResult<PyStrRef> {
424        if !format_spec.is_empty() {
425            return Err(vm.new_type_error(format!(
426                "unsupported format string passed to {}.__format__",
427                obj.class().slot_name()
428            )));
429        }
430        obj.str(vm)
431    }
432
433    #[pygetset]
434    fn __class__(obj: PyObjectRef) -> PyTypeRef {
435        obj.class().to_owned()
436    }
437
438    #[pygetset(setter)]
439    fn set___class__(
440        instance: PyObjectRef,
441        value: PyObjectRef,
442        vm: &VirtualMachine,
443    ) -> PyResult<()> {
444        match value.downcast::<PyType>() {
445            Ok(cls) => {
446                let current_cls = instance.class();
447                let both_module = current_cls.fast_issubclass(vm.ctx.types.module_type)
448                    && cls.fast_issubclass(vm.ctx.types.module_type);
449                let both_mutable = !current_cls
450                    .slots
451                    .flags
452                    .has_feature(PyTypeFlags::IMMUTABLETYPE)
453                    && !cls.slots.flags.has_feature(PyTypeFlags::IMMUTABLETYPE);
454                // FIXME(#1979) cls instances might have a payload
455                if both_mutable || both_module {
456                    super::type_::compatible_for_assignment(current_cls, &cls, "__class__", vm)?;
457                    instance.set_class(cls, vm);
458                    crate::stdlib::_testinternalcapi::note_set_class();
459                    Ok(())
460                } else {
461                    Err(vm.new_type_error(
462                        "__class__ assignment only supported for mutable types or ModuleType subclasses",
463                    ))
464                }
465            }
466            Err(value) => {
467                let value_class = value.class();
468                let type_repr = &value_class.name();
469                Err(vm.new_type_error(format!(
470                    "__class__ must be set to a class, not '{type_repr}' object"
471                )))
472            }
473        }
474    }
475
476    // Return getattr(self, name).
477    //
478    // __getattribute__ is added as a slot wrapper by add_operators.
479    #[pyslot]
480    pub(crate) fn getattro(obj: &PyObject, name: &Py<PyStr>, vm: &VirtualMachine) -> PyResult {
481        vm_trace!("object.__getattribute__({:?}, {:?})", obj, name);
482        obj.as_object().generic_getattr(name, vm)
483    }
484
485    #[pymethod]
486    fn __reduce__(zelf: PyObjectRef, vm: &VirtualMachine) -> PyResult {
487        common_reduce(zelf, 0, vm)
488    }
489
490    #[pymethod]
491    fn __reduce_ex__(zelf: PyObjectRef, protocol: usize, vm: &VirtualMachine) -> PyResult {
492        let __reduce__ = identifier!(vm, __reduce__);
493        if let Some(reduce) = vm.get_attribute_opt(&zelf, __reduce__)? {
494            let object_reduce = vm.ctx.types.object_type.get_attr(__reduce__).unwrap();
495            let typ_obj: PyObjectRef = zelf.class().to_owned().into();
496            let class_reduce = typ_obj.get_attr(__reduce__, vm)?;
497            if !class_reduce.is(&object_reduce) {
498                return reduce.call((), vm);
499            }
500        }
501        common_reduce(zelf, protocol, vm)
502    }
503
504    #[expect(clippy::unnecessary_wraps, reason = "Needs to comply with a signature")]
505    #[pyslot]
506    fn slot_hash(zelf: &PyObject, _vm: &VirtualMachine) -> PyResult<PyHash> {
507        Ok(zelf.get_id() as _)
508    }
509
510    #[pymethod]
511    fn __sizeof__(zelf: PyObjectRef) -> usize {
512        // `slots.basicsize` includes the object header. This reports the payload.
513        zelf.class()
514            .slots()
515            .basicsize
516            .saturating_sub(crate::object::SIZEOF_PYOBJECT_HEAD)
517    }
518}
519
520pub fn object_get_dict(obj: PyObjectRef, vm: &VirtualMachine) -> PyResult<PyDictRef> {
521    if let Some(dict) = obj.dict() {
522        Ok(dict)
523    } else {
524        match obj.instance_dict() {
525            Some(d) => Ok(d.get_or_insert(vm)),
526            None => Err(vm.new_attribute_error("This object has no __dict__")),
527        }
528    }
529}
530pub(crate) fn object_set_dict(
531    obj: PyObjectRef,
532    value: PySetterValue,
533    vm: &VirtualMachine,
534) -> PyResult<()> {
535    let dict = match value {
536        PySetterValue::Assign(value) => Some(downcast_dict(value, vm)?),
537        PySetterValue::Delete => None,
538    };
539    if let Some(instance_dict) = obj.instance_dict() {
540        instance_dict.invalidate_inline_values();
541    }
542    obj.set_dict(dict)
543        .map_err(|_| vm.new_attribute_error("This object has no __dict__"))
544}
545
546/// The dictionary an object holds its attributes in, which is one thing and
547/// one thing only. = subtype_setdict
548fn downcast_dict(value: PyObjectRef, vm: &VirtualMachine) -> PyResult<PyDictRef> {
549    value.downcast::<PyDict>().map_err(|value| {
550        vm.new_type_error(format!(
551            "__dict__ must be set to a dictionary, not a '{}'",
552            value.class().name()
553        ))
554    })
555}
556
557/// = PyObject_GenericSetDict
558pub fn object_generic_set_dict(
559    obj: PyObjectRef,
560    value: PySetterValue,
561    vm: &VirtualMachine,
562) -> PyResult<()> {
563    if matches!(value, PySetterValue::Delete) {
564        return Err(vm.new_type_error("cannot delete __dict__"));
565    }
566    object_set_dict(obj, value, vm)
567}
568
569pub(crate) fn init(ctx: &'static Context) {
570    // Manually set alloc/init slots - derive macro doesn't generate extend_slots
571    // for trait impl that overrides #[pyslot] method
572    ctx.types
573        .object_type
574        .slots()
575        .alloc
576        .store(Some(generic_alloc));
577    ctx.types
578        .object_type
579        .slots
580        .init
581        .store(Some(<PyBaseObject as Initializer>::slot_init));
582    PyBaseObject::extend_class(ctx, ctx.types.object_type);
583}
584
585/// Get arguments for __new__ from __getnewargs_ex__ or __getnewargs__
586/// Returns (args, kwargs) tuple where either can be None
587fn get_new_arguments(
588    obj: &PyObject,
589    vm: &VirtualMachine,
590) -> PyResult<(Option<super::PyTupleRef>, Option<super::PyDictRef>)> {
591    // First try __getnewargs_ex__
592    if let Some(getnewargs_ex) = vm.get_special_method(obj, identifier!(vm, __getnewargs_ex__))? {
593        let newargs = getnewargs_ex.invoke((), vm)?;
594
595        let newargs_tuple: PyRef<super::PyTuple> = newargs.downcast().map_err(|obj| {
596            vm.new_type_error(format!(
597                "__getnewargs_ex__ should return a tuple, not '{}'",
598                obj.class().name()
599            ))
600        })?;
601
602        if newargs_tuple.as_slice().len() != 2 {
603            return Err(vm.new_value_error(format!(
604                "__getnewargs_ex__ should return a tuple of length 2, not {}",
605                newargs_tuple.as_slice().len()
606            )));
607        }
608
609        let args = newargs_tuple.as_slice()[0].clone();
610        let kwargs = newargs_tuple.as_slice()[1].clone();
611
612        let args_tuple: PyRef<super::PyTuple> = args.downcast().map_err(|obj| {
613            vm.new_type_error(format!(
614                "first item of the tuple returned by __getnewargs_ex__ must be a tuple, not '{}'",
615                obj.class().name()
616            ))
617        })?;
618
619        let kwargs_dict: PyRef<super::PyDict> = kwargs.downcast().map_err(|obj| {
620            vm.new_type_error(format!(
621                "second item of the tuple returned by __getnewargs_ex__ must be a dict, not '{}'",
622                obj.class().name()
623            ))
624        })?;
625
626        return Ok((Some(args_tuple), Some(kwargs_dict)));
627    }
628
629    // Fall back to __getnewargs__
630    if let Some(getnewargs) = vm.get_special_method(obj, identifier!(vm, __getnewargs__))? {
631        let args = getnewargs.invoke((), vm)?;
632
633        let args_tuple: PyRef<super::PyTuple> = args.downcast().map_err(|obj| {
634            vm.new_type_error(format!(
635                "__getnewargs__ should return a tuple, not '{}'",
636                obj.class().name()
637            ))
638        })?;
639
640        return Ok((Some(args_tuple), None));
641    }
642
643    // No __getnewargs_ex__ or __getnewargs__
644    Ok((None, None))
645}
646
647/// Check if __getstate__ is overridden by comparing with object.__getstate__
648fn is_getstate_overridden(obj: &PyObject, vm: &VirtualMachine) -> bool {
649    let obj_cls = obj.class();
650    let object_type = vm.ctx.types.object_type;
651
652    // If the class is object itself, not overridden
653    if obj_cls.is(object_type) {
654        return false;
655    }
656
657    // Check if __getstate__ in the MRO comes from object or elsewhere
658    // If the type has its own __getstate__, it's overridden
659    if let Some(getstate) = obj_cls.get_attr(identifier!(vm, __getstate__))
660        && let Some(obj_getstate) = object_type.get_attr(identifier!(vm, __getstate__))
661    {
662        return !getstate.is(&obj_getstate);
663    }
664    false
665}
666
667/// object_getstate - calls __getstate__ method or default implementation
668fn object_getstate(obj: &PyObject, required: bool, vm: &VirtualMachine) -> PyResult {
669    // If __getstate__ is not overridden, use the default implementation with required flag
670    if !is_getstate_overridden(obj, vm) {
671        return object_getstate_default(obj, required, vm);
672    }
673
674    // __getstate__ is overridden, call it without required
675    let getstate = obj.get_attr(identifier!(vm, __getstate__), vm)?;
676    getstate.call((), vm)
677}
678
679/// Get list items iterator if obj is a list (or subclass), None iterator otherwise
680fn get_items_iter(obj: &PyObject, vm: &VirtualMachine) -> PyResult<(PyObjectRef, PyObjectRef)> {
681    let listitems: PyObjectRef = if obj.fast_isinstance(vm.ctx.types.list_type) {
682        obj.get_iter(vm)?.into()
683    } else {
684        vm.ctx.none()
685    };
686
687    let dictitems: PyObjectRef = if obj.fast_isinstance(vm.ctx.types.dict_type) {
688        let items = vm.call_method(obj, "items", ())?;
689        items.get_iter(vm)?.into()
690    } else {
691        vm.ctx.none()
692    };
693
694    Ok((listitems, dictitems))
695}
696
697/// reduce_newobj - creates reduce tuple for protocol >= 2
698fn reduce_newobj(obj: &PyObject, vm: &VirtualMachine) -> PyResult {
699    // Check if type has tp_new
700    let cls = obj.class();
701    if cls.slots.new.load().is_none() {
702        return Err(vm.new_type_error(format!("cannot pickle '{}' object", cls.name())));
703    }
704
705    let (args, kwargs) = get_new_arguments(obj, vm)?;
706
707    let copyreg = vm.import("copyreg", 0)?;
708
709    let has_args = args.is_some();
710
711    let (newobj, newargs): (PyObjectRef, PyObjectRef) = if kwargs.is_none()
712        || kwargs.as_ref().is_some_and(|k| k.is_empty())
713    {
714        // Use copyreg.__newobj__
715        let newobj = copyreg.get_attr("__newobj__", vm)?;
716
717        let args_vec: Vec<PyObjectRef> = args.map(|a| a.as_slice().to_vec()).unwrap_or_default();
718
719        // Create (cls, *args) tuple
720        let mut newargs_vec: Vec<PyObjectRef> = vec![cls.to_owned().into()];
721        newargs_vec.extend(args_vec);
722        let newargs = vm.ctx.new_tuple(newargs_vec);
723
724        (newobj, newargs.into())
725    } else {
726        // args == NULL with non-empty kwargs is BadInternalCall
727        let Some(args) = args else {
728            return Err(vm.new_system_error("bad internal call"));
729        };
730        // Use copyreg.__newobj_ex__
731        let newobj = copyreg.get_attr("__newobj_ex__", vm)?;
732        let args_tuple: PyObjectRef = args.into();
733        let kwargs_dict: PyObjectRef =
734            kwargs.map_or_else(|| vm.ctx.new_dict().into(), |k| k.into());
735
736        let newargs = vm
737            .ctx
738            .new_tuple(vec![cls.to_owned().into(), args_tuple, kwargs_dict]);
739        (newobj, newargs.into())
740    };
741
742    // Determine if state is required
743    // required = !(has_args || is_list || is_dict)
744    let is_list = obj.fast_isinstance(vm.ctx.types.list_type);
745    let is_dict = obj.fast_isinstance(vm.ctx.types.dict_type);
746    let required = !(has_args || is_list || is_dict);
747
748    let state = object_getstate(obj, required, vm)?;
749
750    let (listitems, dictitems) = get_items_iter(obj, vm)?;
751
752    let result = vm
753        .ctx
754        .new_tuple(vec![newobj, newargs, state, listitems, dictitems]);
755    Ok(result.into())
756}
757
758fn common_reduce(obj: PyObjectRef, proto: usize, vm: &VirtualMachine) -> PyResult {
759    if proto >= 2 {
760        reduce_newobj(&obj, vm)
761    } else {
762        let copyreg = vm.import("copyreg", 0)?;
763        let reduce_ex = copyreg.get_attr("_reduce_ex", vm)?;
764        reduce_ex.call((obj, proto), vm)
765    }
766}