Skip to main content

rustpython_vm/builtins/
classmethod.rs

1use super::{PyBoundMethod, PyGenericAlias, PyStr, PyType, PyTypeRef};
2use crate::{
3    AsObject, Context, Py, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine,
4    class::PyClassImpl,
5    common::lock::PyMutex,
6    function::{FuncArgs, PySetterValue},
7    types::{Constructor, GetDescriptor, Initializer, Representable},
8};
9
10/// classmethod(function) -> method
11///
12/// Convert a function to be a class method.
13///
14/// A class method receives the class as implicit first argument,
15/// just like an instance method receives the instance.
16/// To declare a class method, use this idiom:
17///
18///   class C:
19///       @classmethod
20///       def f(cls, arg1, arg2, ...):
21///           ...
22///
23/// It can be called either on the class (e.g. C.f()) or on an instance
24/// (e.g. C().f()).  The instance is ignored except for its class.
25/// If a class method is called for a derived class, the derived class
26/// object is passed as the implied first argument.
27///
28/// Class methods are different than C++ or Java static methods.
29/// If you want those, see the staticmethod builtin.
30#[pyclass(module = false, name = "classmethod")]
31#[derive(Debug)]
32pub struct PyClassMethod {
33    callable: PyMutex<PyObjectRef>,
34}
35
36impl From<PyObjectRef> for PyClassMethod {
37    fn from(callable: PyObjectRef) -> Self {
38        Self {
39            callable: PyMutex::new(callable),
40        }
41    }
42}
43
44impl PyPayload for PyClassMethod {
45    #[inline]
46    fn class(ctx: &Context) -> &'static Py<PyType> {
47        ctx.types.classmethod_type
48    }
49}
50
51impl GetDescriptor for PyClassMethod {
52    fn descr_get(
53        zelf: PyObjectRef,
54        obj: Option<PyObjectRef>,
55        cls: Option<PyObjectRef>,
56        vm: &VirtualMachine,
57    ) -> PyResult {
58        let (zelf, _obj) = Self::_unwrap(&zelf, obj, vm)?;
59        let cls = cls.unwrap_or_else(|| _obj.class().to_owned().into());
60        let callable = zelf.callable.lock().clone();
61        Ok(PyBoundMethod::new(cls, callable).into_ref(&vm.ctx).into())
62    }
63}
64
65impl Constructor for PyClassMethod {
66    type Args = PyObjectRef;
67
68    fn slot_new(cls: PyTypeRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult {
69        let callable: Self::Args = args.bind(vm)?;
70        // Create a dictionary to hold copied attributes
71        let dict = vm.ctx.new_dict();
72
73        // Copy attributes from the callable to the dict
74        // This is similar to functools.wraps in CPython
75        if let Ok(doc) = callable.get_attr("__doc__", vm) {
76            dict.set_item(identifier!(vm.ctx, __doc__), doc, vm)?;
77        }
78        if let Ok(name) = callable.get_attr("__name__", vm) {
79            dict.set_item(identifier!(vm.ctx, __name__), name, vm)?;
80        }
81        if let Ok(qualname) = callable.get_attr("__qualname__", vm) {
82            dict.set_item(identifier!(vm.ctx, __qualname__), qualname, vm)?;
83        }
84        if let Ok(module) = callable.get_attr("__module__", vm) {
85            dict.set_item(identifier!(vm.ctx, __module__), module, vm)?;
86        }
87        if let Ok(annotations) = callable.get_attr("__annotations__", vm) {
88            dict.set_item(identifier!(vm.ctx, __annotations__), annotations, vm)?;
89        }
90
91        // Create PyClassMethod instance with the pre-populated dict
92        let classmethod = Self {
93            callable: PyMutex::new(callable),
94        };
95
96        let result = PyRef::new_ref(classmethod, cls, Some(dict));
97        Ok(PyObjectRef::from(result))
98    }
99
100    fn py_new(_cls: &Py<PyType>, _args: Self::Args, _vm: &VirtualMachine) -> PyResult<Self> {
101        unimplemented!("use slot_new")
102    }
103}
104
105impl Initializer for PyClassMethod {
106    type Args = PyObjectRef;
107
108    fn init(zelf: PyRef<Self>, callable: Self::Args, _vm: &VirtualMachine) -> PyResult<()> {
109        *zelf.callable.lock() = callable;
110        Ok(())
111    }
112}
113
114impl PyClassMethod {
115    #[deprecated(note = "use PyClassMethod::from(...).into_ref() instead")]
116    pub fn new_ref(callable: PyObjectRef, ctx: &Context) -> PyRef<Self> {
117        Self::from(callable).into_ref(ctx)
118    }
119}
120
121#[pyclass(
122    with(GetDescriptor, Constructor, Initializer, Representable),
123    flags(BASETYPE, HAS_DICT, HAS_WEAKREF)
124)]
125impl PyClassMethod {
126    #[pygetset]
127    fn __func__(&self) -> PyObjectRef {
128        self.callable.lock().clone()
129    }
130
131    #[pygetset]
132    fn __wrapped__(&self) -> PyObjectRef {
133        self.callable.lock().clone()
134    }
135
136    #[pygetset]
137    fn __module__(&self, vm: &VirtualMachine) -> PyResult {
138        self.callable.lock().get_attr("__module__", vm)
139    }
140
141    #[pygetset]
142    fn __qualname__(&self, vm: &VirtualMachine) -> PyResult {
143        self.callable.lock().get_attr("__qualname__", vm)
144    }
145
146    #[pygetset]
147    fn __name__(&self, vm: &VirtualMachine) -> PyResult {
148        self.callable.lock().get_attr("__name__", vm)
149    }
150
151    #[pygetset]
152    fn __annotations__(&self, vm: &VirtualMachine) -> PyResult {
153        self.callable.lock().get_attr("__annotations__", vm)
154    }
155
156    #[pygetset(setter)]
157    fn set___annotations__(&self, value: PySetterValue, vm: &VirtualMachine) -> PyResult<()> {
158        match value {
159            PySetterValue::Assign(v) => self.callable.lock().set_attr("__annotations__", v, vm),
160            PySetterValue::Delete => Ok(()), // Silently ignore delete like CPython
161        }
162    }
163
164    #[pygetset]
165    fn __annotate__(&self, vm: &VirtualMachine) -> PyResult {
166        self.callable.lock().get_attr("__annotate__", vm)
167    }
168
169    #[pygetset(setter)]
170    fn set___annotate__(&self, value: PySetterValue, vm: &VirtualMachine) -> PyResult<()> {
171        match value {
172            PySetterValue::Assign(v) => self.callable.lock().set_attr("__annotate__", v, vm),
173            PySetterValue::Delete => Ok(()), // Silently ignore delete like CPython
174        }
175    }
176
177    #[pygetset]
178    fn __isabstractmethod__(&self, vm: &VirtualMachine) -> PyObjectRef {
179        match vm.get_attribute_opt(self.callable.lock().clone(), "__isabstractmethod__") {
180            Ok(Some(is_abstract)) => is_abstract,
181            _ => vm.ctx.new_bool(false).into(),
182        }
183    }
184
185    #[pygetset(setter)]
186    fn set___isabstractmethod__(&self, value: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> {
187        self.callable
188            .lock()
189            .set_attr("__isabstractmethod__", value, vm)?;
190        Ok(())
191    }
192
193    #[pyclassmethod]
194    fn __class_getitem__(cls: PyTypeRef, args: PyObjectRef, vm: &VirtualMachine) -> PyGenericAlias {
195        PyGenericAlias::from_args(cls, args, vm)
196    }
197}
198
199impl Representable for PyClassMethod {
200    #[inline]
201    fn repr_str(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<String> {
202        let callable = zelf.callable.lock().repr(vm).unwrap();
203        let class = Self::class(&vm.ctx);
204
205        let repr = match (
206            class
207                .__qualname__(vm)
208                .downcast_ref::<PyStr>()
209                .map(|n| n.as_wtf8()),
210            class
211                .__module__(vm)
212                .downcast_ref::<PyStr>()
213                .map(|m| m.as_wtf8()),
214        ) {
215            (None, _) => return Err(vm.new_type_error("Unknown qualified name")),
216            (Some(qualname), Some(module)) if module != "builtins" => {
217                format!("<{module}.{qualname}({callable})>")
218            }
219            _ => format!("<{}({})>", class.slot_name(), callable),
220        };
221        Ok(repr)
222    }
223}
224
225pub(crate) fn init(context: &'static Context) {
226    PyClassMethod::extend_class(context, context.types.classmethod_type);
227}