Skip to content

Commit 647534d

Browse files
termoshttclaude
andauthored
#[deprecated] does not work with #[getter] (#256)
## Summary Fixes a bug where `#[deprecated]` attributes on getter/setter methods were not being translated to `@typing_extensions.deprecated` decorators in generated Python stub files. **Before:** ```python @Property def deprecated_getter(self) -> int: ... ``` **After:** ```python @typing_extensions.deprecated("[Since 1.0.0] This method is deprecated") @Property def deprecated_getter(self) -> int: ... ``` ## Root Cause The `MemberInfo` and `MemberDef` structs lacked a `deprecated` field, causing deprecation information to be lost during stub generation. While regular methods (`#[pymethods]`) correctly handled deprecation, getter/setter methods (`#[getter]`/`#[setter]`) did not. ## Changes ### Core Infrastructure - **Type definitions**: Added `deprecated: Option<DeprecatedInfo>` field to `MemberInfo` and `MemberDef` structs - **Proc macro**: Modified member extraction to collect `#[deprecated]` attributes using existing `extract_deprecated()` function - **Type imports**: Added automatic `typing_extensions` import when deprecated members are present ### Code Generation - **Getters**: Updated `GetterDisplay` to output deprecation decorators before `@property` - **Setters**: Updated `SetterDisplay` to output deprecation decorators before `@{name}.setter` - **Constants**: Added warning log for deprecated constants (cannot have decorators in Python syntax) ### Comprehensive Test Coverage Added examples for all PyO3 item types that support `#[deprecated]`: | Item Type | Rust Code | Generated Python Stub | |-----------|-----------|----------------------| | **Function** | `#[deprecated] #[pyfunction] fn deprecated_function()` | `@typing_extensions.deprecated(...) def deprecated_function()` | | **Method** | `#[deprecated] fn deprecated_method(&self)` | `@typing_extensions.deprecated(...) def deprecated_method(self)` | | **Getter** | `#[deprecated] #[getter] fn deprecated_getter(&self)` | `@typing_extensions.deprecated(...) @Property def deprecated_getter(self)` | | **Setter** | `#[deprecated] #[setter] fn set_y(&mut self, value)` | `@typing_extensions.deprecated(...) @y.setter def y(self, value)` | | **Classmethod** | `#[deprecated] #[classmethod] fn deprecated_classmethod()` | `@typing_extensions.deprecated(...) @classmethod def deprecated_classmethod(cls)` | | **Staticmethod** | `#[deprecated] #[staticmethod] fn deprecated_staticmethod()` | `@typing_extensions.deprecated(...) @staticmethod def deprecated_staticmethod()` | ### Constants Handling Constants with `#[deprecated]` are handled specially: - **Collection**: Deprecated attributes are collected from constants during proc macro processing - **Warning**: A helpful warning is logged during stub generation: ``` WARN: Ignoring #[deprecated] on constant 'NUM3': Python constants cannot have decorators. Consider using a function instead if deprecation is needed. ``` - **No output**: No deprecated decorator is generated (would cause Python syntax error) ## Example Usage ```rust #[gen_stub_pymethods] #[pymethods] impl MyClass { #[deprecated(since = "1.0.0", note = "Use new_method instead")] #[getter] fn old_property(&self) -> i32 { self.value } #[deprecated(since = "1.0.0", note = "This constant is deprecated")] #[classattr] const OLD_CONSTANT: i32 = 42; // Will show warning } ``` Generated stub: ```python class MyClass: OLD_CONSTANT: int = 42 # No decorator (with warning logged) @typing_extensions.deprecated("[Since 1.0.0] Use new_method instead") @Property def old_property(self) -> int: ... ``` ## Verification - ✅ All existing tests pass - ✅ Type checking (pyright) passes - ✅ Generated stubs include proper deprecation decorators - ✅ Warning system works for unsupported scenarios - ✅ Backward compatibility maintained - ✅ Comprehensive coverage of all PyO3 deprecated item types --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent cdd9d7a commit 647534d

File tree

10 files changed

+122
-8
lines changed

10 files changed

+122
-8
lines changed

examples/pure/pure.pyi

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ class A:
1616
r"""
1717
class attribute NUM1
1818
"""
19+
NUM3: builtins.int = 3
20+
r"""
21+
deprecated class attribute NUM3 (will show warning)
22+
"""
1923
NUM2: builtins.int
2024
r"""
2125
class attribute NUM2
@@ -27,26 +31,40 @@ class A:
2731
default = 2
2832
```
2933
"""
34+
@property
35+
def y(self) -> builtins.int: ...
36+
@typing_extensions.deprecated("[Since 1.0.0] This method is deprecated")
37+
@property
38+
def deprecated_getter(self) -> builtins.int: ...
3039
@x.setter
3140
def x(self, value: builtins.int) -> None:
3241
r"""
3342
```python
3443
default = 2
3544
```
3645
"""
46+
@typing_extensions.deprecated("[Since 1.0.0] This setter is deprecated")
47+
@y.setter
48+
def y(self, value: builtins.int) -> None: ...
3749
def __new__(cls, x:builtins.int) -> A:
3850
r"""
3951
This is a constructor of :class:`A`.
4052
"""
4153
@classmethod
4254
def classmethod_test1(cls) -> None: ...
55+
@typing_extensions.deprecated("[Since 1.0.0] This classmethod is deprecated")
56+
@classmethod
57+
def deprecated_classmethod(cls) -> None: ...
4358
@classmethod
4459
def classmethod_test2(cls) -> None: ...
4560
def show_x(self) -> None: ...
4661
def ref_test(self, x:dict) -> dict: ...
4762
async def async_get_x(self) -> builtins.int: ...
4863
@typing_extensions.deprecated("[Since 1.0.0] This method is deprecated")
4964
def deprecated_method(self) -> None: ...
65+
@typing_extensions.deprecated("[Since 1.0.0] This staticmethod is deprecated")
66+
@staticmethod
67+
def deprecated_staticmethod() -> builtins.int: ...
5068

5169
class B(A):
5270
...

examples/pure/src/lib.rs

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,14 @@ struct A {
5959
#[gen_stub(default = A::default().x)]
6060
#[pyo3(get, set)]
6161
x: usize,
62+
63+
#[pyo3(get)]
64+
y: usize,
6265
}
6366

6467
impl Default for A {
6568
fn default() -> Self {
66-
Self { x: 2 }
69+
Self { x: 2, y: 10 }
6770
}
6871
}
6972

@@ -73,11 +76,16 @@ impl A {
7376
/// This is a constructor of :class:`A`.
7477
#[new]
7578
fn new(x: usize) -> Self {
76-
Self { x }
79+
Self { x, y: 10 }
7780
}
7881
/// class attribute NUM1
7982
#[classattr]
8083
const NUM1: usize = 2;
84+
85+
/// deprecated class attribute NUM3 (will show warning)
86+
#[deprecated(since = "1.0.0", note = "This constant is deprecated")]
87+
#[classattr]
88+
const NUM3: usize = 3;
8189
/// class attribute NUM2
8290
#[expect(non_snake_case)]
8391
#[classattr]
@@ -89,6 +97,12 @@ impl A {
8997
_ = cls;
9098
}
9199

100+
#[deprecated(since = "1.0.0", note = "This classmethod is deprecated")]
101+
#[classmethod]
102+
fn deprecated_classmethod(cls: &Bound<'_, PyType>) {
103+
_ = cls;
104+
}
105+
92106
#[classmethod]
93107
fn classmethod_test2(_: &Bound<'_, PyType>) {}
94108

@@ -111,13 +125,31 @@ impl A {
111125
fn deprecated_method(&self) {
112126
println!("This method is deprecated");
113127
}
128+
129+
#[deprecated(since = "1.0.0", note = "This method is deprecated")]
130+
#[getter]
131+
fn deprecated_getter(&self) -> usize {
132+
self.x
133+
}
134+
135+
#[deprecated(since = "1.0.0", note = "This setter is deprecated")]
136+
#[setter]
137+
fn set_y(&mut self, value: usize) {
138+
self.y = value;
139+
}
140+
141+
#[deprecated(since = "1.0.0", note = "This staticmethod is deprecated")]
142+
#[staticmethod]
143+
fn deprecated_staticmethod() -> usize {
144+
42
145+
}
114146
}
115147

116148
#[gen_stub_pyfunction]
117149
#[pyfunction]
118150
#[pyo3(signature = (x = 2))]
119151
fn create_a(x: usize) -> A {
120-
A { x }
152+
A { x, y: 10 }
121153
}
122154

123155
#[gen_stub_pyclass]

pyo3-stub-gen-derive/src/gen_stub.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,21 @@
1717
//! r#type: <String as ::pyo3_stub_gen::PyStubType>::type_output,
1818
//! doc: "",
1919
//! default: None,
20+
//! deprecated: None,
2021
//! },
2122
//! MemberInfo {
2223
//! name: "ndim",
2324
//! r#type: <usize as ::pyo3_stub_gen::PyStubType>::type_output,
2425
//! doc: "",
2526
//! default: None,
27+
//! deprecated: None,
2628
//! },
2729
//! MemberInfo {
2830
//! name: "description",
2931
//! r#type: <Option<String> as ::pyo3_stub_gen::PyStubType>::type_output,
3032
//! doc: "",
3133
//! default: None,
34+
//! deprecated: None,
3235
//! },
3336
//! ],
3437
//! setters: &[],

pyo3-stub-gen-derive/src/gen_stub/member.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ pub struct MemberInfo {
1717
name: String,
1818
r#type: TypeOrOverride,
1919
default: Option<Expr>,
20+
deprecated: Option<crate::gen_stub::attr::DeprecatedInfo>,
2021
}
2122

2223
impl MemberInfo {
@@ -67,6 +68,7 @@ impl MemberInfo {
6768
r#type: extract_return_type(&sig.output, attrs)?
6869
.expect("Getter must return a type"),
6970
default,
71+
deprecated: crate::gen_stub::attr::extract_deprecated(attrs),
7072
});
7173
}
7274
}
@@ -112,6 +114,7 @@ impl MemberInfo {
112114
name: name.unwrap_or(fn_setter_name),
113115
r#type,
114116
default,
117+
deprecated: crate::gen_stub::attr::extract_deprecated(attrs),
115118
});
116119
}
117120
}
@@ -127,6 +130,7 @@ impl MemberInfo {
127130
name: sig.ident.to_string(),
128131
r#type: extract_return_type(&sig.output, attrs)?.expect("Getter must return a type"),
129132
default,
133+
deprecated: crate::gen_stub::attr::extract_deprecated(attrs),
130134
})
131135
}
132136
pub fn new_classattr_const(item: ImplItemConst) -> Result<Self> {
@@ -144,6 +148,7 @@ impl MemberInfo {
144148
name: ident.to_string(),
145149
r#type: TypeOrOverride::RustType { r#type: ty },
146150
default: Some(expr),
151+
deprecated: crate::gen_stub::attr::extract_deprecated(&attrs),
147152
})
148153
}
149154
}
@@ -162,11 +167,13 @@ impl TryFrom<Field> for MemberInfo {
162167
}
163168
let doc = extract_documents(&attrs).join("\n");
164169
let default = parse_gen_stub_default(&attrs)?;
170+
let deprecated = crate::gen_stub::attr::extract_deprecated(&attrs);
165171
Ok(Self {
166172
name: field_name.unwrap_or(ident.unwrap().to_string()),
167173
r#type: TypeOrOverride::RustType { r#type: ty },
168174
doc,
169175
default,
176+
deprecated,
170177
})
171178
}
172179
}
@@ -178,6 +185,7 @@ impl ToTokens for MemberInfo {
178185
r#type,
179186
doc,
180187
default,
188+
deprecated,
181189
} = self;
182190
let default = default
183191
.as_ref()
@@ -206,13 +214,25 @@ impl ToTokens for MemberInfo {
206214
&DEFAULT
207215
})}
208216
});
217+
let deprecated_info = deprecated
218+
.as_ref()
219+
.map(|deprecated| {
220+
quote! {
221+
Some(::pyo3_stub_gen::type_info::DeprecatedInfo {
222+
since: #deprecated.since,
223+
note: #deprecated.note,
224+
})
225+
}
226+
})
227+
.unwrap_or_else(|| quote! { None });
209228
match r#type {
210229
TypeOrOverride::RustType { r#type: ty } => tokens.append_all(quote! {
211230
::pyo3_stub_gen::type_info::MemberInfo {
212231
name: #name,
213232
r#type: <#ty as ::pyo3_stub_gen::PyStubType>::type_output,
214233
doc: #doc,
215234
default: #default,
235+
deprecated: #deprecated_info,
216236
}
217237
}),
218238
TypeOrOverride::OverrideType {
@@ -225,6 +245,7 @@ impl ToTokens for MemberInfo {
225245
r#type: || ::pyo3_stub_gen::TypeInfo { name: #type_repr.to_string(), import: ::std::collections::HashSet::from([#(#imports.into(),)*]) },
226246
doc: #doc,
227247
default: #default,
248+
deprecated: #deprecated_info,
228249
}
229250
})
230251
}

pyo3-stub-gen-derive/src/gen_stub/pyclass.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ mod test {
141141
"#,
142142
)?;
143143
let out = PyClassInfo::try_from(input)?.to_token_stream();
144-
insta::assert_snapshot!(format_as_value(out), @r#"
144+
insta::assert_snapshot!(format_as_value(out), @r###"
145145
::pyo3_stub_gen::type_info::PyClassInfo {
146146
pyclass_name: "Placeholder",
147147
struct_id: std::any::TypeId::of::<PyPlaceholder>,
@@ -151,26 +151,29 @@ mod test {
151151
r#type: <String as ::pyo3_stub_gen::PyStubType>::type_output,
152152
doc: "",
153153
default: None,
154+
deprecated: None,
154155
},
155156
::pyo3_stub_gen::type_info::MemberInfo {
156157
name: "ndim",
157158
r#type: <usize as ::pyo3_stub_gen::PyStubType>::type_output,
158159
doc: "",
159160
default: None,
161+
deprecated: None,
160162
},
161163
::pyo3_stub_gen::type_info::MemberInfo {
162164
name: "description",
163165
r#type: <Option<String> as ::pyo3_stub_gen::PyStubType>::type_output,
164166
doc: "",
165167
default: None,
168+
deprecated: None,
166169
},
167170
],
168171
setters: &[],
169172
module: Some("my_module"),
170173
doc: "",
171174
bases: &[],
172175
}
173-
"#);
176+
"###);
174177
Ok(())
175178
}
176179

pyo3-stub-gen-derive/src/gen_stub/pyclass_complex_enum.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ mod test {
120120
"#,
121121
)?;
122122
let out = PyComplexEnumInfo::try_from(input)?.to_token_stream();
123-
insta::assert_snapshot!(format_as_value(out), @r#"
123+
insta::assert_snapshot!(format_as_value(out), @r###"
124124
::pyo3_stub_gen::type_info::PyComplexEnumInfo {
125125
pyclass_name: "Placeholder",
126126
enum_id: std::any::TypeId::of::<PyPlaceholder>,
@@ -133,6 +133,7 @@ mod test {
133133
r#type: <String as ::pyo3_stub_gen::PyStubType>::type_output,
134134
doc: "",
135135
default: None,
136+
deprecated: None,
136137
},
137138
],
138139
module: None,
@@ -154,12 +155,14 @@ mod test {
154155
r#type: <i32 as ::pyo3_stub_gen::PyStubType>::type_output,
155156
doc: "",
156157
default: None,
158+
deprecated: None,
157159
},
158160
::pyo3_stub_gen::type_info::MemberInfo {
159161
name: "_1",
160162
r#type: <f64 as ::pyo3_stub_gen::PyStubType>::type_output,
161163
doc: "",
162164
default: None,
165+
deprecated: None,
163166
},
164167
],
165168
module: None,
@@ -198,6 +201,7 @@ mod test {
198201
r#type: <usize as ::pyo3_stub_gen::PyStubType>::type_output,
199202
doc: "",
200203
default: None,
204+
deprecated: None,
201205
},
202206
],
203207
module: None,
@@ -223,7 +227,7 @@ mod test {
223227
module: Some("my_module"),
224228
doc: "",
225229
}
226-
"#);
230+
"###);
227231
Ok(())
228232
}
229233

0 commit comments

Comments
 (0)