Skip to content

Commit 09cf30c

Browse files
authored
Introspection: expose #[classattribute] as an attribute (#5839)
* Introspection: expose `#[classattribute]` as an attribute Outputs valid but a bit too broad stubs Combining @classmethod and @Property has been removed in 3.13 * Add Final
1 parent c1f3d34 commit 09cf30c

4 files changed

Lines changed: 21 additions & 15 deletions

File tree

newsfragments/5839.changed.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Stubs: emit `#[classattribute]` as plain Python class attributes and not functions annotated with `@classattribute` and `@property` (not supported since 3.13)

pyo3-macros-backend/src/pyimpl.rs

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ use syn::{
2626
ImplItemFn, Result,
2727
};
2828
#[cfg(feature = "experimental-inspect")]
29-
use syn::{parse_quote, Ident};
29+
use syn::{parse_quote, Ident, ReturnType};
3030

3131
/// The mechanism used to collect `#[pymethods]` into the type object
3232
#[derive(Copy, Clone)]
@@ -462,10 +462,20 @@ pub fn method_introspection_code(
462462
}
463463
FnType::FnModule(_) => (), // TODO: not sure this can happen
464464
FnType::ClassAttribute => {
465-
first_argument = Some("cls");
466-
// TODO: this combination only works with Python 3.9-3.11 https://docs.python.org/3.11/library/functions.html#classmethod
467-
decorators.push(PyExpr::builtin("classmethod"));
468-
decorators.push(PyExpr::builtin("property"));
465+
// We return an attribute because there is no decorator for this case
466+
return attribute_introspection_code(
467+
pyo3_path,
468+
Some(parent),
469+
name,
470+
PyExpr::ellipsis(),
471+
if let ReturnType::Type(_, t) = &spec.output {
472+
(**t).clone()
473+
} else {
474+
parse_quote!(#pyo3_path::Py<#pyo3_path::types::PyNone>)
475+
},
476+
get_doc(attrs, None).as_ref(),
477+
true,
478+
);
469479
}
470480
}
471481
let return_type = if spec.python_name == "__new__" {

pytests/noxfile.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,16 +49,13 @@ def mypy(session: nox.Session):
4949
session.install(".[dev]")
5050

5151
# TODO: remove --disable-error-code", "override" when __eq__ and __ne__ will always take object for input
52-
# TODO: remove "--disable-error-code", "misc" when #[classattr] will be properly emitted
5352
session.run_always(
5453
"python",
5554
"-m",
5655
"mypy",
5756
"tests",
5857
"--disable-error-code",
5958
"override",
60-
"--disable-error-code",
61-
"misc",
6259
)
6360
# TODO: enable stubtest when previously listed errors will be fixed session.run_always("python", "-m", "mypy.stubtest", "pyo3_pytests")
6461
finally:

pytests/stubs/pyclasses.pyi

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from _typeshed import Incomplete
2-
from typing import final
2+
from typing import Final, final
33

44
class AssertingBaseClass:
55
"""
@@ -9,6 +9,10 @@ class AssertingBaseClass:
99

1010
@final
1111
class ClassWithDecorators:
12+
cls_attribute: Final[int]
13+
"""
14+
A class attribute
15+
"""
1216
def __new__(cls, /) -> ClassWithDecorators: ...
1317
@property
1418
def attr(self, /) -> int:
@@ -26,12 +30,6 @@ class ClassWithDecorators:
2630
A setter
2731
"""
2832
@classmethod
29-
@property
30-
def cls_attribute(cls, /) -> int:
31-
"""
32-
A class attribute
33-
"""
34-
@classmethod
3533
def cls_method(cls, /) -> int:
3634
"""
3735
A class method

0 commit comments

Comments
 (0)