Skip to main content

rustpython_vm/builtins/
property.rs

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