use super::{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::{Either, FuncArgs, 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
}
}
impl Constructor for PyBaseObject {
type Args = FuncArgs;
fn slot_new(cls: PyTypeRef, args: Self::Args, 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 Some(unimplemented_abstract_method_count) = abs_methods.length_opt(vm)
{
let methods: Vec<PyUtf8StrRef> = abs_methods.try_to_value(vm)?;
let methods: String = Itertools::intersperse(
methods.iter().map(|name| name.as_str().to_owned()),
"', '".to_owned(),
)
.collect();
let unimplemented_abstract_method_count = unimplemented_abstract_method_count?;
let name = cls.name().to_string();
match unimplemented_abstract_method_count {
0 => {}
1 => {
return Err(vm.new_type_error(format!(
"class {name} without an implementation for abstract method '{methods}'"
)));
}
2.. => {
return Err(vm.new_type_error(format!(
"class {name} without an implementation for abstract methods '{methods}'"
)));
}
#[allow(unreachable_patterns)]
_ => unreachable!(),
}
}
generic_alloc(cls, 0, vm)
}
fn py_new(_cls: &Py<PyType>, _args: Self::Args, _vm: &VirtualMachine) -> PyResult<Self> {
unimplemented!("use slot_new")
}
}
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 = FuncArgs;
fn slot_init(zelf: PyObjectRef, 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| f as usize);
let object_init = object_type.slots.init.load().map(|f| f as usize);
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: PyRef<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>> {
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).map_err(|_| vm.new_type_error("cannot pickle object"))?;
if required {
let mut basicsize = vm.ctx.types.object_type.slots.basicsize;
if obj.class().slots.flags.has_feature(PyTypeFlags::HAS_DICT) {
basicsize += core::mem::size_of::<PyObjectRef>();
}
let has_weakref = if let Some(ref ext) = obj.class().heaptype_ext {
match &ext.slots {
None => true, Some(slots) => slots.iter().any(|s| s.as_bytes() == b"__weakref__"),
}
} else {
let weakref_name = vm.ctx.intern_str("__weakref__");
obj.class().attributes.read().contains_key(weakref_name)
};
if has_weakref {
basicsize += core::mem::size_of::<PyObjectRef>();
}
if let Some(ref slot_names) = slot_names {
basicsize += core::mem::size_of::<PyObjectRef>() * slot_names.__len__();
}
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(raw)]
fn __getstate__(vm: &VirtualMachine, args: FuncArgs) -> PyResult {
let (zelf,): (PyObjectRef,) = args.bind(vm)?;
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)
}
#[pymethod]
fn __setattr__(
obj: PyObjectRef,
name: PyStrRef,
value: PyObjectRef,
vm: &VirtualMachine,
) -> PyResult<()> {
obj.generic_setattr(&name, PySetterValue::Assign(value), vm)
}
#[pymethod]
fn __delattr__(obj: PyObjectRef, name: PyStrRef, vm: &VirtualMachine) -> PyResult<()> {
obj.generic_setattr(&name, PySetterValue::Delete, vm)
}
#[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();
match (
class
.__qualname__(vm)
.downcast_ref::<PyStr>()
.map(|n| n.as_wtf8()),
class
.__module__(vm)
.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__(_args: FuncArgs, vm: &VirtualMachine) -> PyObjectRef {
vm.ctx.not_implemented()
}
#[pyclassmethod]
fn __init_subclass__(_cls: PyTypeRef) {}
#[pymethod]
pub fn __dir__(obj: PyObjectRef, vm: &VirtualMachine) -> PyResult<PyList> {
obj.dir(vm)
}
#[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().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 {
let has_dict =
|typ: &Py<PyType>| typ.slots.flags.has_feature(PyTypeFlags::HAS_DICT);
let has_weakref =
|typ: &Py<PyType>| typ.slots.flags.has_feature(PyTypeFlags::HAS_WEAKREF);
let slots_equal = match (
current_cls
.heaptype_ext
.as_ref()
.and_then(|e| e.slots.as_ref()),
cls.heaptype_ext.as_ref().and_then(|e| e.slots.as_ref()),
) {
(Some(a), Some(b)) => {
a.len() == b.len()
&& a.iter()
.zip(b.iter())
.all(|(x, y)| x.as_wtf8() == y.as_wtf8())
}
(None, None) => true,
_ => false,
};
if current_cls.slots.basicsize != cls.slots.basicsize
|| !slots_equal
|| has_dict(current_cls) != has_dict(&cls)
|| has_weakref(current_cls) != has_weakref(&cls)
|| current_cls.slots.member_count != cls.slots.member_count
{
return Err(vm.new_type_error(format!(
"__class__ assignment: '{}' object layout differs from '{}'",
cls.name(),
current_cls.name()
)));
}
instance.set_class(cls, vm);
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 __getattribute__(obj: PyObjectRef, name: PyStrRef, vm: &VirtualMachine) -> PyResult {
Self::getattro(&obj, &name, vm)
}
#[pymethod]
fn __reduce__(obj: PyObjectRef, vm: &VirtualMachine) -> PyResult {
common_reduce(obj, 0, vm)
}
#[pymethod]
fn __reduce_ex__(obj: PyObjectRef, proto: usize, vm: &VirtualMachine) -> PyResult {
let __reduce__ = identifier!(vm, __reduce__);
if let Some(reduce) = vm.get_attribute_opt(obj.clone(), __reduce__)? {
let object_reduce = vm.ctx.types.object_type.get_attr(__reduce__).unwrap();
let typ_obj: PyObjectRef = obj.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(obj, proto, vm)
}
#[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
}
}
pub fn object_get_dict(obj: PyObjectRef, vm: &VirtualMachine) -> PyResult<PyDictRef> {
obj.dict()
.ok_or_else(|| vm.new_attribute_error("This object has no __dict__"))
}
pub fn object_set_dict(obj: PyObjectRef, dict: PyDictRef, vm: &VirtualMachine) -> PyResult<()> {
obj.set_dict(dict)
.map_err(|_| vm.new_attribute_error("This object has no __dict__"))
}
pub 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.len() != 2 {
return Err(vm.new_value_error(format!(
"__getnewargs_ex__ should return a tuple of length 2, not {}",
newargs_tuple.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: &PyObjectRef, 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: PyObjectRef, 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(|k| k.into())
.unwrap_or_else(|| vm.ctx.new_dict().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)
}
}