Skip to content

Commit d904450

Browse files
Tptdavidhewitt
andauthored
Implement deleters (#5699)
* Implement deleters Adds a #[deleter] macro When deleter is set, the `PyGetSetDef.setter` function calls the setter or deleter depending on if the value is `NULL` or not Issue #5686 * Update src/impl_/pymethods.rs Co-authored-by: David Hewitt <[email protected]> --------- Co-authored-by: David Hewitt <[email protected]>
1 parent b69c767 commit d904450

File tree

12 files changed

+310
-57
lines changed

12 files changed

+310
-57
lines changed

guide/src/class.md

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ Below is a list of links to the relevant section of this chapter for each:
1414
- [`#[new]`](#constructor)
1515
- [`#[getter]`](#object-properties-using-getter-and-setter)
1616
- [`#[setter]`](#object-properties-using-getter-and-setter)
17+
- [`#[deleter]`](#object-properties-using-getter-and-setter)
1718
- [`#[staticmethod]`](#static-methods)
1819
- [`#[classmethod]`](#class-methods)
1920
- [`#[classattr]`](#class-attributes)
@@ -620,7 +621,7 @@ Here, the `args` and `kwargs` allow creating instances of the subclass passing i
620621
PyO3 supports two ways to add properties to your `#[pyclass]`:
621622

622623
- For simple struct fields with no side effects, a `#[pyo3(get, set)]` attribute can be added directly to the field definition in the `#[pyclass]`.
623-
- For properties which require computation you can define `#[getter]` and `#[setter]` functions in the [`#[pymethods]`](#instance-methods) block.
624+
- For properties which require computation you can define `#[getter]`, `#[setter]` and `#[deleter]` functions in the [`#[pymethods]`](#instance-methods) block.
624625

625626
We'll cover each of these in the following sections.
626627

@@ -670,13 +671,43 @@ impl MyClass {
670671
fn num(&self) -> PyResult<i32> {
671672
Ok(self.num)
672673
}
674+
675+
#[setter]
676+
fn set_num(&mut self, num: i32) {
677+
self.num = num;
678+
}
679+
}
680+
```
681+
682+
The `#[deleter]` attribute is also available to delete the property.
683+
This corresponds to the `del my_object.my_property` python operation.
684+
685+
```rust
686+
# use pyo3::prelude::*;
687+
# use pyo3::exceptions::PyAttributeError;
688+
#[pyclass]
689+
struct MyClass {
690+
num: Option<i32>,
691+
}
692+
693+
#[pymethods]
694+
impl MyClass {
695+
#[getter]
696+
fn num(&self) -> PyResult<i32> {
697+
self.num.ok_or_else(|| PyAttributeError::new_err("num has been deleted"))
698+
}
699+
700+
#[deleter]
701+
fn delete_num(&mut self) {
702+
self.num = None;
703+
}
673704
}
674705
```
675706

676-
A getter or setter's function name is used as the property name by default.
707+
A getter, setter or deleters's function name is used as the property name by default.
677708
There are several ways how to override the name.
678709

679-
If a function name starts with `get_` or `set_` for getter or setter respectively, the descriptor name becomes the function name with this prefix removed.
710+
If a function name starts with `get_`, `set_` or `delete_` for getter, setter or deleter, respectively, the descriptor name becomes the function name with this prefix removed.
680711
This is also useful in case of Rust keywords like `type` ([raw identifiers](https://doc.rust-lang.org/edition-guide/rust-2018/module-system/raw-identifiers.html) can be used since Rust 2018).
681712

682713
```rust
@@ -702,7 +733,7 @@ impl MyClass {
702733

703734
In this case, a property `num` is defined and available from Python code as `self.num`.
704735

705-
Both the `#[getter]` and `#[setter]` attributes accept one parameter.
736+
The `#[getter]`, `#[setter]` and `#[deleter]` attributes accept one parameter.
706737
If this parameter is specified, it is used as the property name, i.e.
707738

708739
```rust
@@ -728,9 +759,6 @@ impl MyClass {
728759

729760
In this case, the property `number` is defined and available from Python code as `self.number`.
730761

731-
Attributes defined by `#[setter]` or `#[pyo3(set)]` will always raise `AttributeError` on `del` operations.
732-
Support for defining custom `del` behavior is tracked in [#1778](https://github.com/PyO3/pyo3/issues/1778).
733-
734762
## Instance methods
735763

736764
To define a Python compatible method, an `impl` block for your struct has to be annotated with the `#[pymethods]` attribute.

newsfragments/5699.added.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
`#[deleter]` attribute to implement property deleters

pyo3-macros-backend/src/method.rs

Lines changed: 53 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,8 @@ pub enum FnType {
220220
Getter(SelfType),
221221
/// Represents a pymethod annotated with `#[setter]`
222222
Setter(SelfType),
223+
/// Represents a pymethod annotated with `#[deleter]`
224+
Deleter(SelfType),
223225
/// Represents a regular pymethod
224226
Fn(SelfType),
225227
/// Represents a pymethod annotated with `#[classmethod]`, like a `@classmethod`
@@ -237,6 +239,7 @@ impl FnType {
237239
match self {
238240
FnType::Getter(_)
239241
| FnType::Setter(_)
242+
| FnType::Deleter(_)
240243
| FnType::Fn(_)
241244
| FnType::FnClass(_)
242245
| FnType::FnModule(_) => true,
@@ -247,9 +250,11 @@ impl FnType {
247250
pub fn signature_attribute_allowed(&self) -> bool {
248251
match self {
249252
FnType::Fn(_) | FnType::FnStatic | FnType::FnClass(_) | FnType::FnModule(_) => true,
250-
// Setter, Getter and ClassAttribute all have fixed signatures (either take 0 or 1
253+
// Getter, Setter and Deleter and ClassAttribute all have fixed signatures (either take 0 or 1
251254
// arguments) so cannot have a `signature = (...)` attribute.
252-
FnType::Getter(_) | FnType::Setter(_) | FnType::ClassAttribute => false,
255+
FnType::Getter(_) | FnType::Setter(_) | FnType::Deleter(_) | FnType::ClassAttribute => {
256+
false
257+
}
253258
}
254259
}
255260

@@ -262,12 +267,14 @@ impl FnType {
262267
) -> Option<TokenStream> {
263268
let Ctx { pyo3_path, .. } = ctx;
264269
match self {
265-
FnType::Getter(st) | FnType::Setter(st) | FnType::Fn(st) => Some(st.receiver(
266-
cls.expect("no class given for Fn with a \"self\" receiver"),
267-
error_mode,
268-
holders,
269-
ctx,
270-
)),
270+
FnType::Getter(st) | FnType::Setter(st) | FnType::Deleter(st) | FnType::Fn(st) => {
271+
Some(st.receiver(
272+
cls.expect("no class given for Fn with a \"self\" receiver"),
273+
error_mode,
274+
holders,
275+
ctx,
276+
))
277+
}
271278
FnType::FnClass(span) => {
272279
let py = syn::Ident::new("py", Span::call_site());
273280
let slf: Ident = syn::Ident::new("_slf", Span::call_site());
@@ -592,6 +599,19 @@ impl<'a> FnSpec<'a> {
592599

593600
FnType::Setter(parse_receiver("expected receiver for `#[setter]`")?)
594601
}
602+
[MethodTypeAttribute::Deleter(_, name)] => {
603+
if let Some(name) = name.take() {
604+
ensure_spanned!(
605+
python_name.replace(name).is_none(),
606+
python_name.span() => "`name` may only be specified once"
607+
);
608+
} else if python_name.is_none() {
609+
// Strip off "delete_" prefix if needed
610+
*python_name = strip_fn_name("delete_");
611+
}
612+
613+
FnType::Deleter(parse_receiver("expected receiver for `#[deleter]`")?)
614+
}
595615
[first, rest @ .., last] => {
596616
// Join as many of the spans together as possible
597617
let span = rest
@@ -872,8 +892,10 @@ impl<'a> FnSpec<'a> {
872892
/// and/or attributes. Prepend the callable name to make a complete `__text_signature__`.
873893
pub fn text_signature_call_signature(&self) -> Option<String> {
874894
let self_argument = match &self.tp {
875-
// Getters / Setters / ClassAttribute are not callables on the Python side
876-
FnType::Getter(_) | FnType::Setter(_) | FnType::ClassAttribute => return None,
895+
// Getters / Setters / deleter / ClassAttribute are not callables on the Python side
896+
FnType::Getter(_) | FnType::Setter(_) | FnType::Deleter(_) | FnType::ClassAttribute => {
897+
return None
898+
}
877899
FnType::Fn(_) => Some("self"),
878900
FnType::FnModule(_) => Some("module"),
879901
FnType::FnClass(_) => Some("cls"),
@@ -894,6 +916,7 @@ enum MethodTypeAttribute {
894916
StaticMethod(Span),
895917
Getter(Span, Option<Ident>),
896918
Setter(Span, Option<Ident>),
919+
Deleter(Span, Option<Ident>),
897920
ClassAttribute(Span),
898921
}
899922

@@ -905,6 +928,7 @@ impl MethodTypeAttribute {
905928
| MethodTypeAttribute::StaticMethod(span)
906929
| MethodTypeAttribute::Getter(span, _)
907930
| MethodTypeAttribute::Setter(span, _)
931+
| MethodTypeAttribute::Deleter(span, _)
908932
| MethodTypeAttribute::ClassAttribute(span) => *span,
909933
}
910934
}
@@ -973,6 +997,9 @@ impl MethodTypeAttribute {
973997
} else if path.is_ident("setter") {
974998
let name = extract_name(meta, "setter")?;
975999
Ok(Some(MethodTypeAttribute::Setter(path.span(), name)))
1000+
} else if path.is_ident("deleter") {
1001+
let name = extract_name(meta, "deleter")?;
1002+
Ok(Some(MethodTypeAttribute::Deleter(path.span(), name)))
9761003
} else {
9771004
Ok(None)
9781005
}
@@ -981,14 +1008,15 @@ impl MethodTypeAttribute {
9811008

9821009
impl Display for MethodTypeAttribute {
9831010
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
984-
match self {
985-
MethodTypeAttribute::New(_) => "#[new]".fmt(f),
986-
MethodTypeAttribute::ClassMethod(_) => "#[classmethod]".fmt(f),
987-
MethodTypeAttribute::StaticMethod(_) => "#[staticmethod]".fmt(f),
988-
MethodTypeAttribute::Getter(_, _) => "#[getter]".fmt(f),
989-
MethodTypeAttribute::Setter(_, _) => "#[setter]".fmt(f),
990-
MethodTypeAttribute::ClassAttribute(_) => "#[classattr]".fmt(f),
991-
}
1011+
f.write_str(match self {
1012+
MethodTypeAttribute::New(_) => "#[new]",
1013+
MethodTypeAttribute::ClassMethod(_) => "#[classmethod]",
1014+
MethodTypeAttribute::StaticMethod(_) => "#[staticmethod]",
1015+
MethodTypeAttribute::Getter(_, _) => "#[getter]",
1016+
MethodTypeAttribute::Setter(_, _) => "#[setter]",
1017+
MethodTypeAttribute::Deleter(_, _) => "#[deleter]",
1018+
MethodTypeAttribute::ClassAttribute(_) => "#[classattr]",
1019+
})
9921020
}
9931021
}
9941022

@@ -1028,6 +1056,10 @@ fn ensure_signatures_on_valid_method(
10281056
debug_assert!(!fn_type.signature_attribute_allowed());
10291057
bail_spanned!(signature.kw.span() => "`signature` not allowed with `setter`")
10301058
}
1059+
FnType::Deleter(_) => {
1060+
debug_assert!(!fn_type.signature_attribute_allowed());
1061+
bail_spanned!(signature.kw.span() => "`signature` not allowed with `deleter`")
1062+
}
10311063
FnType::ClassAttribute => {
10321064
debug_assert!(!fn_type.signature_attribute_allowed());
10331065
bail_spanned!(signature.kw.span() => "`signature` not allowed with `classattr`")
@@ -1043,6 +1075,9 @@ fn ensure_signatures_on_valid_method(
10431075
FnType::Setter(_) => {
10441076
bail_spanned!(text_signature.kw.span() => "`text_signature` not allowed with `setter`")
10451077
}
1078+
FnType::Deleter(_) => {
1079+
bail_spanned!(text_signature.kw.span() => "`text_signature` not allowed with `deleter`")
1080+
}
10461081
FnType::ClassAttribute => {
10471082
bail_spanned!(text_signature.kw.span() => "`text_signature` not allowed with `classattr`")
10481083
}

pyo3-macros-backend/src/pyimpl.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,10 @@ fn method_introspection_code(spec: &FnSpec<'_>, parent: &syn::Type, ctx: &Ctx) -
407407
first_argument = Some("self");
408408
decorators.push(PythonTypeHint::local(format!("{name}.setter")));
409409
}
410+
FnType::Deleter(_) => {
411+
first_argument = Some("self");
412+
decorators.push(PythonTypeHint::local(format!("{name}.deleter")));
413+
}
410414
FnType::Fn(_) => {
411415
first_argument = Some("self");
412416
}

pyo3-macros-backend/src/pymethod.rs

Lines changed: 77 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,13 @@ pub fn gen_py_method(
282282
},
283283
ctx,
284284
)?),
285+
(_, FnType::Deleter(self_type)) => GeneratedPyMethod::Method(impl_py_deleter_def(
286+
cls,
287+
self_type,
288+
spec,
289+
spec.get_doc(meth_attrs, ctx)?,
290+
ctx,
291+
)?),
285292
(_, FnType::FnModule(_)) => {
286293
unreachable!("methods cannot be FnModule")
287294
}
@@ -695,10 +702,7 @@ pub fn impl_py_setter_def(
695702
_value: *mut #pyo3_path::ffi::PyObject,
696703
) -> #pyo3_path::PyResult<::std::ffi::c_int> {
697704
use ::std::convert::Into;
698-
let _value = #pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr_or_opt(py, &_value)
699-
.ok_or_else(|| {
700-
#pyo3_path::exceptions::PyAttributeError::new_err("can't delete attribute")
701-
})?;
705+
let _value = #pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr(py, &_value);
702706
#init_holders
703707
#extract
704708
#warnings
@@ -854,6 +858,74 @@ pub fn impl_py_getter_def(
854858
}
855859
}
856860

861+
pub fn impl_py_deleter_def(
862+
cls: &syn::Type,
863+
self_type: &SelfType,
864+
spec: &FnSpec<'_>,
865+
doc: PythonDoc,
866+
ctx: &Ctx,
867+
) -> Result<MethodAndMethodDef> {
868+
let Ctx { pyo3_path, .. } = ctx;
869+
let python_name = spec.null_terminated_python_name();
870+
let mut holders = Holders::new();
871+
let deleter_impl = impl_call_deleter(cls, spec, self_type, &mut holders, ctx)?;
872+
let wrapper_ident = format_ident!("__pymethod_delete_{}__", spec.name);
873+
let warnings = spec.warnings.build_py_warning(ctx);
874+
let init_holders = holders.init_holders(ctx);
875+
let associated_method = quote! {
876+
unsafe fn #wrapper_ident(
877+
py: #pyo3_path::Python<'_>,
878+
_slf: *mut #pyo3_path::ffi::PyObject,
879+
) -> #pyo3_path::PyResult<::std::ffi::c_int> {
880+
#init_holders
881+
#warnings
882+
let result = #deleter_impl;
883+
#pyo3_path::impl_::callback::convert(py, result)
884+
}
885+
};
886+
887+
let method_def = quote! {
888+
#pyo3_path::impl_::pymethods::PyMethodDefType::Deleter(
889+
#pyo3_path::impl_::pymethods::PyDeleterDef::new(
890+
#python_name,
891+
#cls::#wrapper_ident,
892+
#doc
893+
)
894+
)
895+
};
896+
897+
Ok(MethodAndMethodDef {
898+
associated_method,
899+
method_def,
900+
})
901+
}
902+
903+
fn impl_call_deleter(
904+
cls: &syn::Type,
905+
spec: &FnSpec<'_>,
906+
self_type: &SelfType,
907+
holders: &mut Holders,
908+
ctx: &Ctx,
909+
) -> Result<TokenStream> {
910+
let (py_arg, args) = split_off_python_arg(&spec.signature.arguments);
911+
let slf = self_type.receiver(cls, ExtractErrorMode::Raise, holders, ctx);
912+
913+
if !args.is_empty() {
914+
bail_spanned!(spec.name.span() =>
915+
"deleter function can have at most one argument ([pyo3::Python,])"
916+
);
917+
}
918+
919+
let name = &spec.name;
920+
let fncall = if py_arg.is_some() {
921+
quote!(#cls::#name(#slf, py))
922+
} else {
923+
quote!(#cls::#name(#slf))
924+
};
925+
926+
Ok(fncall)
927+
}
928+
857929
/// Split an argument of pyo3::Python from the front of the arg list, if present
858930
fn split_off_python_arg<'a, 'b>(args: &'a [FnArg<'b>]) -> (Option<&'a PyArg<'b>>, &'a [FnArg<'b>]) {
859931
match args {
@@ -865,7 +937,7 @@ fn split_off_python_arg<'a, 'b>(args: &'a [FnArg<'b>]) -> (Option<&'a PyArg<'b>>
865937
pub enum PropertyType<'a> {
866938
Descriptor {
867939
field_index: usize,
868-
field: &'a syn::Field,
940+
field: &'a Field,
869941
python_name: Option<&'a NameAttribute>,
870942
renaming_rule: Option<RenamingRule>,
871943
},

0 commit comments

Comments
 (0)