Skip to main content

rustpython_vm/builtins/
property.rs

1/*! Python `property` descriptor class.
2
3*/
4use super::PyType;
5use crate::common::lock::PyRwLock;
6use crate::object::PyAtomicRef;
7use crate::{
8    AsObject, Context, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine,
9    class::PyClassImpl,
10    function::{ArgumentError, FromArgs, FuncArgs, Param, PySetterValue},
11    types::{Constructor, GetDescriptor, Initializer},
12};
13use core::sync::atomic::{AtomicBool, Ordering};
14
15struct SetNameArgs {
16    _owner: PyObjectRef,
17    name: PyObjectRef,
18}
19
20impl FromArgs for SetNameArgs {
21    const PARAMS: Option<&'static [Param]> = Some(&[
22        Param::positional_only("owner"),
23        Param::positional_only("name"),
24    ]);
25
26    fn from_args(vm: &VirtualMachine, args: &mut FuncArgs) -> Result<Self, ArgumentError> {
27        let [owner, name]: [PyObjectRef; 2] =
28            core::mem::take(&mut args.args)
29                .try_into()
30                .map_err(|args: Vec<PyObjectRef>| {
31                    ArgumentError::Exception(vm.new_type_error(format!(
32                        "__set_name__() takes 2 positional arguments but {} were given",
33                        args.len()
34                    )))
35                })?;
36        Ok(Self {
37            _owner: owner,
38            name,
39        })
40    }
41}
42
43#[pyclass(module = false, name = "property", traverse)]
44#[derive(Debug)]
45pub struct PyProperty {
46    #[pymember(name = "fget")]
47    getter: PyAtomicRef<Option<PyObject>>,
48    #[pymember(name = "fset")]
49    setter: PyAtomicRef<Option<PyObject>>,
50    #[pymember(name = "fdel")]
51    deleter: PyAtomicRef<Option<PyObject>>,
52    #[pymember(name = "__doc__", writable)]
53    doc: PyAtomicRef<Option<PyObject>>,
54    name: PyRwLock<Option<PyObjectRef>>,
55    #[pytraverse(skip)]
56    getter_doc: core::sync::atomic::AtomicBool,
57}
58
59impl PyPayload for PyProperty {
60    #[inline]
61    fn class(ctx: &Context) -> &'static Py<PyType> {
62        ctx.types.property_type
63    }
64}
65
66#[derive(FromArgs)]
67pub struct PropertyArgs {
68    #[pyarg(any, optional)]
69    fget: Option<PyObjectRef>,
70    #[pyarg(any, optional)]
71    fset: Option<PyObjectRef>,
72    #[pyarg(any, optional)]
73    fdel: Option<PyObjectRef>,
74    #[pyarg(any, optional)]
75    doc: Option<PyObjectRef>,
76}
77
78impl GetDescriptor for PyProperty {
79    fn descr_get(
80        zelf_obj: &PyObject,
81        obj: Option<&PyObject>,
82        _cls: Option<&PyObject>,
83        vm: &VirtualMachine,
84    ) -> PyResult {
85        let (zelf, obj) = Self::_unwrap(zelf_obj, obj, vm)?;
86        if vm.is_none(obj) {
87            return Ok(zelf_obj.to_owned());
88        }
89
90        // Clone and release lock before calling Python code to prevent deadlock
91        let value = zelf.getter.load_owned();
92        if let Some(getter) = value {
93            getter.call((obj.to_owned(),), vm)
94        } else {
95            let error_msg = zelf.format_property_error(obj, "getter", vm)?;
96            Err(vm.new_attribute_error(error_msg))
97        }
98    }
99}
100
101impl PyProperty {
102    // Helper method to get property name
103    // Returns the name if available, None if not found, or propagates errors
104    fn get_property_name(&self, vm: &VirtualMachine) -> PyResult<Option<PyObjectRef>> {
105        // First check if name was set via __set_name__
106        let value = self.name.read().clone();
107        if let Some(name) = value {
108            return Ok(Some(name));
109        }
110
111        // Clone and release lock before calling Python code to prevent deadlock
112        let Some(getter) = self.getter.load_owned() else {
113            return Ok(None);
114        };
115
116        match getter.get_attr("__name__", vm) {
117            Ok(name) => Ok(Some(name)),
118            Err(e) => {
119                // If it's an AttributeError from the getter, return None
120                // Otherwise, propagate the original exception (e.g., RuntimeError)
121                if e.class().is(vm.ctx.exceptions.attribute_error) {
122                    Ok(None)
123                } else {
124                    Err(e)
125                }
126            }
127        }
128    }
129
130    pub(crate) fn get_fget(&self) -> Option<PyObjectRef> {
131        self.getter.load_owned()
132    }
133
134    fn get_fset(&self) -> Option<PyObjectRef> {
135        self.setter.load_owned()
136    }
137
138    fn get_fdel(&self) -> Option<PyObjectRef> {
139        self.deleter.load_owned()
140    }
141
142    fn doc_getter(&self) -> Option<PyObjectRef> {
143        self.doc.load_owned()
144    }
145
146    // Python builder functions
147
148    // Helper method to create a new property with updated attributes
149    fn clone_property_with(
150        zelf: &Py<Self>,
151        new_getter: Option<PyObjectRef>,
152        new_setter: Option<PyObjectRef>,
153        new_deleter: Option<PyObjectRef>,
154        vm: &VirtualMachine,
155    ) -> PyResult<PyRef<Self>> {
156        // Determine doc based on getter_doc flag and whether we're updating the getter
157        let doc = if zelf.getter_doc.load(Ordering::Relaxed) && new_getter.is_some() {
158            // If the original property uses getter doc and we have a new getter,
159            // pass Py_None to let __init__ get the doc from the new getter
160            Some(vm.ctx.none())
161        } else if zelf.getter_doc.load(Ordering::Relaxed) {
162            // If original used getter_doc but we're not changing the getter,
163            // pass None to let init get doc from existing getter
164            Some(vm.ctx.none())
165        } else {
166            // Otherwise use the existing doc
167            zelf.doc_getter()
168        };
169
170        // Create property args with updated values
171        let args = PropertyArgs {
172            fget: new_getter.or_else(|| zelf.get_fget()),
173            fset: new_setter.or_else(|| zelf.get_fset()),
174            fdel: new_deleter.or_else(|| zelf.get_fdel()),
175            doc,
176        };
177
178        // Create new property using py_new and init
179        let new_prop = Self::slot_new(zelf.class().to_owned(), FuncArgs::default(), vm)?;
180        let new_prop_ref = new_prop.downcast::<Self>().unwrap();
181        Self::init(&new_prop_ref, args, vm)?;
182
183        // Copy the name if it exists
184        let value = zelf.name.read().clone();
185        if let Some(name) = value {
186            *new_prop_ref.name.write() = Some(name);
187        }
188
189        Ok(new_prop_ref)
190    }
191
192    // Helper method to format property error messages
193    #[cold]
194    fn format_property_error(
195        &self,
196        obj: &PyObject,
197        error_type: &str,
198        vm: &VirtualMachine,
199    ) -> PyResult<String> {
200        let prop_name = self.get_property_name(vm)?;
201        let obj_type = obj.class();
202        let qualname = obj_type.__qualname__(vm);
203
204        match prop_name {
205            Some(name) => Ok(format!(
206                "property {} of {} object has no {}",
207                name.repr(vm)?,
208                qualname.repr(vm)?,
209                error_type
210            )),
211            None => Ok(format!(
212                "property of {} object has no {}",
213                qualname.repr(vm)?,
214                error_type
215            )),
216        }
217    }
218}
219
220#[pyclass(
221    with(Constructor, Initializer, GetDescriptor),
222    flags(BASETYPE, HAS_WEAKREF)
223)]
224impl Py<PyProperty> {
225    // Descriptor methods
226
227    #[pyslot]
228    fn descr_set(
229        zelf: &PyObject,
230        obj: PyObjectRef,
231        value: PySetterValue,
232        vm: &VirtualMachine,
233    ) -> PyResult<()> {
234        let zelf = zelf.try_to_ref::<PyProperty>(vm)?;
235        match value {
236            PySetterValue::Assign(value) => {
237                // Clone and release lock before calling Python code to prevent deadlock
238                let set = zelf.setter.load_owned();
239                if let Some(setter) = set {
240                    setter.call((obj, value), vm).map(drop)
241                } else {
242                    let error_msg = zelf.format_property_error(&obj, "setter", vm)?;
243                    Err(vm.new_attribute_error(error_msg))
244                }
245            }
246            PySetterValue::Delete => {
247                // Clone and release lock before calling Python code to prevent deadlock
248                let del = zelf.deleter.load_owned();
249                if let Some(deleter) = del {
250                    deleter.call((obj,), vm).map(drop)
251                } else {
252                    let error_msg = zelf.format_property_error(&obj, "deleter", vm)?;
253                    Err(vm.new_attribute_error(error_msg))
254                }
255            }
256        }
257    }
258
259    #[pygetset(name = "__name__")]
260    fn name_getter(&self, vm: &VirtualMachine) -> PyResult {
261        match self.get_property_name(vm)? {
262            Some(name) => Ok(name),
263            None => Err(vm.new_attribute_error("'property' object has no attribute '__name__'")),
264        }
265    }
266
267    #[pygetset(name = "__name__", setter)]
268    fn name_setter(&self, value: PyObjectRef) {
269        *self.name.write() = Some(value);
270    }
271
272    #[pymethod]
273    fn __set_name__(&self, SetNameArgs { name, .. }: SetNameArgs) {
274        *self.name.write() = Some(name);
275    }
276
277    #[pymethod]
278    fn getter(
279        zelf: PyRef<PyProperty>,
280        object: Option<PyObjectRef>,
281        vm: &VirtualMachine,
282    ) -> PyResult<PyRef<PyProperty>> {
283        PyProperty::clone_property_with(&zelf, object, None, None, vm)
284    }
285
286    #[pymethod]
287    fn setter(
288        zelf: PyRef<PyProperty>,
289        object: Option<PyObjectRef>,
290        vm: &VirtualMachine,
291    ) -> PyResult<PyRef<PyProperty>> {
292        PyProperty::clone_property_with(&zelf, None, object, None, vm)
293    }
294
295    #[pymethod]
296    fn deleter(
297        zelf: PyRef<PyProperty>,
298        object: Option<PyObjectRef>,
299        vm: &VirtualMachine,
300    ) -> PyResult<PyRef<PyProperty>> {
301        PyProperty::clone_property_with(&zelf, None, None, object, vm)
302    }
303
304    #[pygetset]
305    fn __isabstractmethod__(&self, vm: &VirtualMachine) -> PyResult {
306        // Helper to check if a method is abstract
307        let is_abstract = |method: &PyObject| -> PyResult<bool> {
308            match method.get_attr("__isabstractmethod__", vm) {
309                Ok(isabstract) => isabstract.try_to_bool(vm),
310                Err(_) => Ok(false),
311            }
312        };
313
314        // Clone and release lock before calling Python code to prevent deadlock
315        // Check getter
316        if let Some(getter) = self.getter.load_owned()
317            && is_abstract(&getter)?
318        {
319            return Ok(vm.ctx.new_bool(true).into());
320        }
321
322        // Check setter
323        if let Some(setter) = self.setter.load_owned()
324            && is_abstract(&setter)?
325        {
326            return Ok(vm.ctx.new_bool(true).into());
327        }
328
329        // Check deleter
330        if let Some(deleter) = self.deleter.load_owned()
331            && is_abstract(&deleter)?
332        {
333            return Ok(vm.ctx.new_bool(true).into());
334        }
335
336        Ok(vm.ctx.new_bool(false).into())
337    }
338
339    #[pygetset(setter)]
340    fn set___isabstractmethod__(&self, value: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> {
341        // Clone and release lock before calling Python code to prevent deadlock
342        let maybe_getter = self.getter.load_owned();
343        if let Some(getter) = maybe_getter {
344            getter.set_attr("__isabstractmethod__", value, vm)?;
345        }
346        Ok(())
347    }
348}
349
350impl Constructor for PyProperty {
351    type Args = FuncArgs;
352
353    fn py_new(_cls: &Py<PyType>, _args: FuncArgs, _vm: &VirtualMachine) -> PyResult<Self> {
354        Ok(Self {
355            getter: PyAtomicRef::new_empty(),
356            setter: PyAtomicRef::new_empty(),
357            deleter: PyAtomicRef::new_empty(),
358            doc: PyAtomicRef::new_empty(),
359            name: PyRwLock::new(None),
360            getter_doc: AtomicBool::new(false),
361        })
362    }
363}
364
365impl Initializer for PyProperty {
366    type Args = PropertyArgs;
367
368    fn init(zelf: &Py<Self>, args: Self::Args, vm: &VirtualMachine) -> PyResult<()> {
369        // Set doc and getter_doc flag
370        let mut getter_doc = false;
371
372        // Helper to get doc from getter
373        let get_getter_doc = |fget: &PyObject| -> Option<PyObjectRef> {
374            fget.get_attr("__doc__", vm)
375                .ok()
376                .filter(|doc| !vm.is_none(doc))
377        };
378
379        let doc = match args.doc {
380            Some(doc) if !vm.is_none(&doc) => Some(doc),
381            _ => {
382                // No explicit doc or doc is None, try to get from getter
383                args.fget.as_ref().and_then(|fget| {
384                    get_getter_doc(fget).inspect(|_| {
385                        getter_doc = true;
386                    })
387                })
388            }
389        };
390
391        // Check if this is a property subclass
392        let is_exact_property = zelf.class().is(vm.ctx.types.property_type);
393
394        if is_exact_property {
395            // For exact property type, store doc in the field
396            zelf.doc.store(doc);
397        } else {
398            // For property subclass, set __doc__ as an attribute
399            let doc_to_set = doc.unwrap_or_else(|| vm.ctx.none());
400            match zelf.as_object().set_attr("__doc__", doc_to_set, vm) {
401                Ok(()) => {}
402                Err(e) if !getter_doc && e.class().is(vm.ctx.exceptions.attribute_error) => {
403                    // Silently ignore AttributeError for backwards compatibility
404                    // (only when not using getter_doc)
405                }
406                Err(e) => return Err(e),
407            }
408        }
409
410        zelf.getter.store(args.fget);
411        zelf.setter.store(args.fset);
412        zelf.deleter.store(args.fdel);
413        zelf.getter_doc.store(getter_doc, Ordering::Relaxed);
414
415        Ok(())
416    }
417}
418
419pub(crate) fn init(context: &'static Context) {
420    PyProperty::extend_class(context, context.types.property_type);
421}