use super::{PyDict, PyDictRef, PyList, PyStr, PyStrRef, PyType, PyTypeRef, PyUtf8StrRef};
use crate::common::hash::PyHash;
use crate::types::PyTypeFlags;
use crate::{
AsObject, Context, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine,
class::PyClassImpl,
convert::ToPyResult,
function::{
ArgumentError, Either, FromArgs, FuncArgs, Param, PyArithmeticValue, PyComparisonValue,
PySetterValue,
},
types::{Constructor, Initializer, PyComparisonOp},
};
use itertools::Itertools;
#[pyclass(module = false, name = "object")]
#[derive(Debug)]
pub struct PyBaseObject;
impl PyPayload for PyBaseObject {
#[inline]
fn class(ctx: &Context) -> &'static Py<PyType> {
ctx.types.object_type
}
}
pub struct ObjectArgs;
impl FromArgs for ObjectArgs {
const PARAMS: Option<&'static [Param]> = Some(&[]);
fn from_args(_vm: &VirtualMachine, args: &mut FuncArgs) -> Result<Self, ArgumentError> {
core::mem::take(args);
Ok(Self)
}
}
impl Constructor for PyBaseObject {
type Args = ObjectArgs;
fn slot_new(cls: PyTypeRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult {
if !args.args.is_empty() || !args.kwargs.is_empty() {
let tp_new = cls.get_attr(identifier!(vm, __new__));
let object_new = vm.ctx.types.object_type.get_attr(identifier!(vm, __new__));
if let (Some(tp_new), Some(object_new)) = (tp_new, object_new) {
if !tp_new.is(&object_new) {
return Err(vm.new_type_error(
"object.__new__() takes exactly one argument (the type to instantiate)",
));
}
let tp_init = cls.get_attr(identifier!(vm, __init__));
let object_init = vm.ctx.types.object_type.get_attr(identifier!(vm, __init__));
if let (Some(tp_init), Some(object_init)) = (tp_init, object_init)
&& tp_init.is(&object_init)
{
return Err(vm.new_type_error(format!("{}() takes no arguments", cls.name())));
}
}
}
if let Some(abs_methods) = cls.get_attr(identifier!(vm, __abstractmethods__)) {
let methods: Vec<PyUtf8StrRef> = abs_methods.try_to_value(vm)?;
let unimplemented_abstract_method_count = methods.len();
if unimplemented_abstract_method_count > 0 {
let methods: String = Itertools::intersperse(
methods.iter().map(|name| name.as_str().to_owned()),
"', '".to_owned(),
)
.collect();
let name = cls.name().to_string();
let noun = if unimplemented_abstract_method_count == 1 {
"method"
} else {
"methods"
};
return Err(vm.new_type_error(format!(
"class {name} without an implementation for abstract {noun} '{methods}'"
)));
}
}
generic_alloc(cls, 0, vm)
}
fn py_new(_cls: &Py<PyType>, _args: Self::Args, _vm: &VirtualMachine) -> PyResult<Self> {
unimplemented!("use slot_new")
}
}
#[expect(clippy::unnecessary_wraps, reason = "Needs to comply with a signature")]
pub(crate) fn generic_alloc(cls: PyTypeRef, _nitems: usize, vm: &VirtualMachine) -> PyResult {
let dict = if cls
.slots
.flags
.has_feature(crate::types::PyTypeFlags::HAS_DICT)
{
Some(vm.ctx.new_dict())
} else {
None
};
Ok(crate::PyRef::new_ref(PyBaseObject, cls, dict).into())
}
impl Initializer for PyBaseObject {
type Args = ObjectArgs;
fn slot_init(zelf: &PyObject, args: FuncArgs, vm: &VirtualMachine) -> PyResult<()> {
if args.is_empty() {
return Ok(());
}
let typ = zelf.class();
let object_type = &vm.ctx.types.object_type;
let typ_init = typ.slots.init.load().map(|f| crate::types::fn_addr(f));
let object_init = object_type
.slots
.init
.load()
.map(|f| crate::types::fn_addr(f));
if typ_init != object_init {
return Err(vm.new_type_error(
"object.__init__() takes exactly one argument (the instance to initialize)",
));
}
if let (Some(typ_new), Some(object_new)) = (
typ.get_attr(identifier!(vm, __new__)),
object_type.get_attr(identifier!(vm, __new__)),
) && typ_new.is(&object_new)
{
return Err(vm.new_type_error(format!(
"{}.__init__() takes exactly one argument (the instance to initialize)",
typ.name()
)));
}
Ok(())
}
fn init(_zelf: &Py<Self>, _args: Self::Args, _vm: &VirtualMachine) -> PyResult<()> {
unreachable!("slot_init is defined")
}
}
fn type_slot_names(typ: &Py<PyType>, vm: &VirtualMachine) -> PyResult<Option<super::PyListRef>> {
if let Some(slot_names) = typ.get_direct_attr(identifier!(vm.ctx, __slotnames__)) {
return match_class!(match slot_names {
l @ super::PyList => Ok(Some(l)),
_n @ super::PyNone => Ok(None),
other => Err(vm.new_type_error(format!(
"{:.200}.__slotnames__ should be a list or None, not {:.200}",
typ.name(),
other.class().name()
))),
});
}
let copyreg = vm.import("copyreg", 0)?;
let copyreg_slotnames = copyreg.get_attr("_slotnames", vm)?;
let slot_names = copyreg_slotnames.call((typ.to_owned(),), vm)?;
let result = match_class!(match slot_names {
l @ super::PyList => Some(l),
_n @ super::PyNone => None,
_ => return Err(vm.new_type_error("copyreg._slotnames didn't return a list or None")),
});
Ok(result)
}
fn object_getstate_default(obj: &PyObject, required: bool, vm: &VirtualMachine) -> PyResult {
if required && obj.class().slots().itemsize > 0 {
return Err(vm.new_type_error(format!("cannot pickle {:.200} objects", obj.class().name())));
}
let state = if obj.dict().is_none_or(|d| d.is_empty()) {
vm.ctx.none()
} else {
let Some(state) = obj.dict() else {
return Ok(vm.ctx.none());
};
state.into()
};
let slot_names = type_slot_names(obj.class(), vm)?;
if required {
let basicsize = vm.ctx.types.object_type.slots().basicsize;
if obj.class().slots().basicsize > basicsize {
return Err(vm.new_type_error(format!("cannot pickle '{}' object", obj.class().name())));
}
}
if let Some(slot_names) = slot_names {
let slot_names_len = slot_names.__len__();
if slot_names_len > 0 {
let slots = vm.ctx.new_dict();
for i in 0..slot_names_len {
let borrowed_names = slot_names.borrow_vec();
if borrowed_names.len() != slot_names_len {
return Err(vm.new_runtime_error("__slotnames__ changed size during iteration"));
}
let name = borrowed_names[i].downcast_ref::<PyStr>().unwrap();
let Ok(value) = obj.get_attr(name, vm) else {
continue;
};
slots.set_item(name.as_wtf8(), value, vm).unwrap();
}
if !slots.is_empty() {
return (state, slots).to_pyresult(vm);
}
}
}
Ok(state)
}
#[pyclass(with(Constructor, Initializer), flags(BASETYPE))]
impl PyBaseObject {
#[pymethod]
fn __getstate__(zelf: PyObjectRef, vm: &VirtualMachine) -> PyResult {
object_getstate_default(&zelf, false, vm)
}
#[pyslot]
fn slot_richcompare(
zelf: &PyObject,
other: &PyObject,
op: PyComparisonOp,
vm: &VirtualMachine,
) -> PyResult<Either<PyObjectRef, PyComparisonValue>> {
Self::cmp(zelf, other, op, vm).map(Either::B)
}
#[inline(always)]
fn cmp(
zelf: &PyObject,
other: &PyObject,
op: PyComparisonOp,
vm: &VirtualMachine,
) -> PyResult<PyComparisonValue> {
let res = match op {
PyComparisonOp::Eq => {
if zelf.is(other) {
PyComparisonValue::Implemented(true)
} else {
PyComparisonValue::NotImplemented
}
}
PyComparisonOp::Ne => {
let cmp = zelf.class().slots().richcompare.load().unwrap();
let value = match cmp(zelf, other, PyComparisonOp::Eq, vm)? {
Either::A(obj) => PyArithmeticValue::from_object(vm, obj)
.map(|obj| obj.try_to_bool(vm))
.transpose()?,
Either::B(value) => value,
};
value.map(|v| !v)
}
_ => PyComparisonValue::NotImplemented,
};
Ok(res)
}
#[pyslot]
pub(crate) fn slot_setattro(
obj: &PyObject,
attr_name: &Py<PyStr>,
value: PySetterValue,
vm: &VirtualMachine,
) -> PyResult<()> {
obj.generic_setattr(attr_name, value, vm)
}
#[pyslot]
fn slot_str(zelf: &PyObject, vm: &VirtualMachine) -> PyResult<PyStrRef> {
zelf.repr(vm)
}
#[pyslot]
fn slot_repr(zelf: &PyObject, vm: &VirtualMachine) -> PyResult<PyStrRef> {
let class = zelf.class();
let module = class.__module__(vm).ok();
match (
class
.__qualname__(vm)
.downcast_ref::<PyStr>()
.map(|n| n.as_wtf8()),
module
.as_ref()
.and_then(|m| m.downcast_ref::<PyStr>())
.map(|m| m.as_wtf8()),
) {
(None, _) => Err(vm.new_type_error("Unknown qualified name")),
(Some(qualname), Some(module)) if module != "builtins" => Ok(PyStr::from(format!(
"<{}.{} object at {:#x}>",
module,
qualname,
zelf.get_id()
))
.into_ref(&vm.ctx)),
_ => Ok(PyStr::from(format!(
"<{} object at {:#x}>",
class.slot_name(),
zelf.get_id()
))
.into_ref(&vm.ctx)),
}
}
#[pyclassmethod]
fn __subclasshook__(_cls: PyTypeRef, _object: PyObjectRef, vm: &VirtualMachine) -> PyObjectRef {
vm.ctx.not_implemented()
}
#[pyclassmethod]
fn __init_subclass__(_cls: PyTypeRef) {}
#[pymethod]
pub fn __dir__(zelf: PyObjectRef, vm: &VirtualMachine) -> PyResult<PyList> {
let mut names: Vec<PyObjectRef> = Vec::new();
match zelf.get_attr(identifier!(vm, __dict__), vm) {
Ok(obj) => {
if let Ok(dict) = obj.downcast::<PyDict>() {
names.extend(
dict.into_iter()
.filter_map(|(k, _)| k.downcast_ref::<PyStr>().is_some().then_some(k)),
);
}
}
Err(e) if e.fast_isinstance(vm.ctx.exceptions.attribute_error) => {}
Err(e) => return Err(e),
}
match zelf.get_attr(identifier!(vm, __class__), vm) {
Ok(cls_obj) => {
if let Some(cls) = cls_obj.downcast_ref::<PyType>() {
for (name, _) in cls.get_attributes(&vm.ctx) {
names.push(name.to_object());
}
}
}
Err(e) if e.fast_isinstance(vm.ctx.exceptions.attribute_error) => {}
Err(e) => return Err(e),
}
let lst = PyList::from(names);
lst.sort(Default::default(), vm)?;
Ok(lst)
}
#[pymethod]
fn __format__(
obj: PyObjectRef,
format_spec: PyStrRef,
vm: &VirtualMachine,
) -> PyResult<PyStrRef> {
if !format_spec.is_empty() {
return Err(vm.new_type_error(format!(
"unsupported format string passed to {}.__format__",
obj.class().slot_name()
)));
}
obj.str(vm)
}
#[pygetset]
fn __class__(obj: PyObjectRef) -> PyTypeRef {
obj.class().to_owned()
}
#[pygetset(setter)]
fn set___class__(
instance: PyObjectRef,
value: PyObjectRef,
vm: &VirtualMachine,
) -> PyResult<()> {
match value.downcast::<PyType>() {
Ok(cls) => {
let current_cls = instance.class();
let both_module = current_cls.fast_issubclass(vm.ctx.types.module_type)
&& cls.fast_issubclass(vm.ctx.types.module_type);
let both_mutable = !current_cls
.slots
.flags
.has_feature(PyTypeFlags::IMMUTABLETYPE)
&& !cls.slots.flags.has_feature(PyTypeFlags::IMMUTABLETYPE);
if both_mutable || both_module {
super::type_::compatible_for_assignment(current_cls, &cls, "__class__", vm)?;
instance.set_class(cls, vm);
crate::stdlib::_testinternalcapi::note_set_class();
Ok(())
} else {
Err(vm.new_type_error(
"__class__ assignment only supported for mutable types or ModuleType subclasses",
))
}
}
Err(value) => {
let value_class = value.class();
let type_repr = &value_class.name();
Err(vm.new_type_error(format!(
"__class__ must be set to a class, not '{type_repr}' object"
)))
}
}
}
#[pyslot]
pub(crate) fn getattro(obj: &PyObject, name: &Py<PyStr>, vm: &VirtualMachine) -> PyResult {
vm_trace!("object.__getattribute__({:?}, {:?})", obj, name);
obj.as_object().generic_getattr(name, vm)
}
#[pymethod]
fn __reduce__(zelf: PyObjectRef, vm: &VirtualMachine) -> PyResult {
common_reduce(zelf, 0, vm)
}
#[pymethod]
fn __reduce_ex__(zelf: PyObjectRef, protocol: usize, vm: &VirtualMachine) -> PyResult {
let __reduce__ = identifier!(vm, __reduce__);
if let Some(reduce) = vm.get_attribute_opt(&zelf, __reduce__)? {
let object_reduce = vm.ctx.types.object_type.get_attr(__reduce__).unwrap();
let typ_obj: PyObjectRef = zelf.class().to_owned().into();
let class_reduce = typ_obj.get_attr(__reduce__, vm)?;
if !class_reduce.is(&object_reduce) {
return reduce.call((), vm);
}
}
common_reduce(zelf, protocol, vm)
}
#[expect(clippy::unnecessary_wraps, reason = "Needs to comply with a signature")]
#[pyslot]
fn slot_hash(zelf: &PyObject, _vm: &VirtualMachine) -> PyResult<PyHash> {
Ok(zelf.get_id() as _)
}
#[pymethod]
fn __sizeof__(zelf: PyObjectRef) -> usize {
zelf.class()
.slots()
.basicsize
.saturating_sub(crate::object::SIZEOF_PYOBJECT_HEAD)
}
}
pub fn object_get_dict(obj: PyObjectRef, vm: &VirtualMachine) -> PyResult<PyDictRef> {
if let Some(dict) = obj.dict() {
Ok(dict)
} else {
match obj.instance_dict() {
Some(d) => Ok(d.get_or_insert(vm)),
None => Err(vm.new_attribute_error("This object has no __dict__")),
}
}
}
pub(crate) fn object_set_dict(
obj: PyObjectRef,
value: PySetterValue,
vm: &VirtualMachine,
) -> PyResult<()> {
let dict = match value {
PySetterValue::Assign(value) => Some(downcast_dict(value, vm)?),
PySetterValue::Delete => None,
};
if let Some(instance_dict) = obj.instance_dict() {
instance_dict.invalidate_inline_values();
}
obj.set_dict(dict)
.map_err(|_| vm.new_attribute_error("This object has no __dict__"))
}
fn downcast_dict(value: PyObjectRef, vm: &VirtualMachine) -> PyResult<PyDictRef> {
value.downcast::<PyDict>().map_err(|value| {
vm.new_type_error(format!(
"__dict__ must be set to a dictionary, not a '{}'",
value.class().name()
))
})
}
pub fn object_generic_set_dict(
obj: PyObjectRef,
value: PySetterValue,
vm: &VirtualMachine,
) -> PyResult<()> {
if matches!(value, PySetterValue::Delete) {
return Err(vm.new_type_error("cannot delete __dict__"));
}
object_set_dict(obj, value, vm)
}
pub(crate) fn init(ctx: &'static Context) {
ctx.types
.object_type
.slots()
.alloc
.store(Some(generic_alloc));
ctx.types
.object_type
.slots
.init
.store(Some(<PyBaseObject as Initializer>::slot_init));
PyBaseObject::extend_class(ctx, ctx.types.object_type);
}
fn get_new_arguments(
obj: &PyObject,
vm: &VirtualMachine,
) -> PyResult<(Option<super::PyTupleRef>, Option<super::PyDictRef>)> {
if let Some(getnewargs_ex) = vm.get_special_method(obj, identifier!(vm, __getnewargs_ex__))? {
let newargs = getnewargs_ex.invoke((), vm)?;
let newargs_tuple: PyRef<super::PyTuple> = newargs.downcast().map_err(|obj| {
vm.new_type_error(format!(
"__getnewargs_ex__ should return a tuple, not '{}'",
obj.class().name()
))
})?;
if newargs_tuple.as_slice().len() != 2 {
return Err(vm.new_value_error(format!(
"__getnewargs_ex__ should return a tuple of length 2, not {}",
newargs_tuple.as_slice().len()
)));
}
let args = newargs_tuple.as_slice()[0].clone();
let kwargs = newargs_tuple.as_slice()[1].clone();
let args_tuple: PyRef<super::PyTuple> = args.downcast().map_err(|obj| {
vm.new_type_error(format!(
"first item of the tuple returned by __getnewargs_ex__ must be a tuple, not '{}'",
obj.class().name()
))
})?;
let kwargs_dict: PyRef<super::PyDict> = kwargs.downcast().map_err(|obj| {
vm.new_type_error(format!(
"second item of the tuple returned by __getnewargs_ex__ must be a dict, not '{}'",
obj.class().name()
))
})?;
return Ok((Some(args_tuple), Some(kwargs_dict)));
}
if let Some(getnewargs) = vm.get_special_method(obj, identifier!(vm, __getnewargs__))? {
let args = getnewargs.invoke((), vm)?;
let args_tuple: PyRef<super::PyTuple> = args.downcast().map_err(|obj| {
vm.new_type_error(format!(
"__getnewargs__ should return a tuple, not '{}'",
obj.class().name()
))
})?;
return Ok((Some(args_tuple), None));
}
Ok((None, None))
}
fn is_getstate_overridden(obj: &PyObject, vm: &VirtualMachine) -> bool {
let obj_cls = obj.class();
let object_type = vm.ctx.types.object_type;
if obj_cls.is(object_type) {
return false;
}
if let Some(getstate) = obj_cls.get_attr(identifier!(vm, __getstate__))
&& let Some(obj_getstate) = object_type.get_attr(identifier!(vm, __getstate__))
{
return !getstate.is(&obj_getstate);
}
false
}
fn object_getstate(obj: &PyObject, required: bool, vm: &VirtualMachine) -> PyResult {
if !is_getstate_overridden(obj, vm) {
return object_getstate_default(obj, required, vm);
}
let getstate = obj.get_attr(identifier!(vm, __getstate__), vm)?;
getstate.call((), vm)
}
fn get_items_iter(obj: &PyObject, vm: &VirtualMachine) -> PyResult<(PyObjectRef, PyObjectRef)> {
let listitems: PyObjectRef = if obj.fast_isinstance(vm.ctx.types.list_type) {
obj.get_iter(vm)?.into()
} else {
vm.ctx.none()
};
let dictitems: PyObjectRef = if obj.fast_isinstance(vm.ctx.types.dict_type) {
let items = vm.call_method(obj, "items", ())?;
items.get_iter(vm)?.into()
} else {
vm.ctx.none()
};
Ok((listitems, dictitems))
}
fn reduce_newobj(obj: &PyObject, vm: &VirtualMachine) -> PyResult {
let cls = obj.class();
if cls.slots.new.load().is_none() {
return Err(vm.new_type_error(format!("cannot pickle '{}' object", cls.name())));
}
let (args, kwargs) = get_new_arguments(obj, vm)?;
let copyreg = vm.import("copyreg", 0)?;
let has_args = args.is_some();
let (newobj, newargs): (PyObjectRef, PyObjectRef) = if kwargs.is_none()
|| kwargs.as_ref().is_some_and(|k| k.is_empty())
{
let newobj = copyreg.get_attr("__newobj__", vm)?;
let args_vec: Vec<PyObjectRef> = args.map(|a| a.as_slice().to_vec()).unwrap_or_default();
let mut newargs_vec: Vec<PyObjectRef> = vec![cls.to_owned().into()];
newargs_vec.extend(args_vec);
let newargs = vm.ctx.new_tuple(newargs_vec);
(newobj, newargs.into())
} else {
let Some(args) = args else {
return Err(vm.new_system_error("bad internal call"));
};
let newobj = copyreg.get_attr("__newobj_ex__", vm)?;
let args_tuple: PyObjectRef = args.into();
let kwargs_dict: PyObjectRef =
kwargs.map_or_else(|| vm.ctx.new_dict().into(), |k| k.into());
let newargs = vm
.ctx
.new_tuple(vec![cls.to_owned().into(), args_tuple, kwargs_dict]);
(newobj, newargs.into())
};
let is_list = obj.fast_isinstance(vm.ctx.types.list_type);
let is_dict = obj.fast_isinstance(vm.ctx.types.dict_type);
let required = !(has_args || is_list || is_dict);
let state = object_getstate(obj, required, vm)?;
let (listitems, dictitems) = get_items_iter(obj, vm)?;
let result = vm
.ctx
.new_tuple(vec![newobj, newargs, state, listitems, dictitems]);
Ok(result.into())
}
fn common_reduce(obj: PyObjectRef, proto: usize, vm: &VirtualMachine) -> PyResult {
if proto >= 2 {
reduce_newobj(&obj, vm)
} else {
let copyreg = vm.import("copyreg", 0)?;
let reduce_ex = copyreg.get_attr("_reduce_ex", vm)?;
reduce_ex.call((obj, proto), vm)
}
}