Skip to content

Commit b1a21cc

Browse files
authored
Introspection: add typing.final on final classes (#5552)
* Introspection: add typing.final on final classes by introducing decorators on classes * Improve error message
1 parent 2d37bb5 commit b1a21cc

File tree

13 files changed

+121
-50
lines changed

13 files changed

+121
-50
lines changed

newsfragments/5552.added.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Introspection: `@typing.final` on final classes

pyo3-introspection/src/introspection.rs

Lines changed: 35 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -124,9 +124,17 @@ fn convert_members<'a>(
124124
chunks_by_parent,
125125
)?);
126126
}
127-
Chunk::Class { name, id } => {
128-
classes.push(convert_class(id, name, chunks_by_id, chunks_by_parent)?)
129-
}
127+
Chunk::Class {
128+
name,
129+
id,
130+
decorators,
131+
} => classes.push(convert_class(
132+
id,
133+
name,
134+
decorators,
135+
chunks_by_id,
136+
chunks_by_parent,
137+
)?),
130138
Chunk::Function {
131139
name,
132140
id: _,
@@ -178,6 +186,7 @@ fn convert_members<'a>(
178186
fn convert_class(
179187
id: &str,
180188
name: &str,
189+
decorators: &[ChunkTypeHint],
181190
chunks_by_id: &HashMap<&str, &Chunk>,
182191
chunks_by_parent: &HashMap<&str, Vec<&Chunk>>,
183192
) -> Result<Class> {
@@ -198,9 +207,29 @@ fn convert_class(
198207
name: name.into(),
199208
methods,
200209
attributes,
210+
decorators: decorators
211+
.iter()
212+
.map(convert_decorator)
213+
.collect::<Result<_>>()?,
201214
})
202215
}
203216

217+
fn convert_decorator(decorator: &ChunkTypeHint) -> Result<PythonIdentifier> {
218+
match convert_type_hint(decorator) {
219+
TypeHint::Plain(id) => Ok(PythonIdentifier {
220+
module: None,
221+
name: id.clone(),
222+
}),
223+
TypeHint::Ast(expr) => {
224+
if let TypeHintExpr::Identifier(i) = expr {
225+
Ok(i)
226+
} else {
227+
bail!("PyO3 introspection currently only support decorators that are identifiers of a Python function")
228+
}
229+
}
230+
}
231+
}
232+
204233
fn convert_function(
205234
name: &str,
206235
arguments: &ChunkArguments,
@@ -211,19 +240,7 @@ fn convert_function(
211240
name: name.into(),
212241
decorators: decorators
213242
.iter()
214-
.map(|d| match convert_type_hint(d) {
215-
TypeHint::Plain(id) => Ok(PythonIdentifier {
216-
module: None,
217-
name: id.clone(),
218-
}),
219-
TypeHint::Ast(expr) => {
220-
if let TypeHintExpr::Identifier(i) = expr {
221-
Ok(i)
222-
} else {
223-
bail!("A decorator must be the identifier of a Python function")
224-
}
225-
}
226-
})
243+
.map(convert_decorator)
227244
.collect::<Result<_>>()?,
228245
arguments: Arguments {
229246
positional_only_arguments: arguments.posonlyargs.iter().map(convert_argument).collect(),
@@ -444,6 +461,8 @@ enum Chunk {
444461
Class {
445462
id: String,
446463
name: String,
464+
#[serde(default)]
465+
decorators: Vec<ChunkTypeHint>,
447466
},
448467
Function {
449468
#[serde(default)]

pyo3-introspection/src/model.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ pub struct Class {
1313
pub name: String,
1414
pub methods: Vec<Function>,
1515
pub attributes: Vec<Attribute>,
16+
/// decorator like 'typing.final'
17+
pub decorators: Vec<PythonIdentifier>,
1618
}
1719

1820
#[derive(Debug, Eq, PartialEq, Clone, Hash)]

pyo3-introspection/src/stubs.rs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,15 @@ fn module_stubs(module: &Module, parents: &[&str]) -> String {
119119
}
120120

121121
fn class_stubs(class: &Class, imports: &Imports) -> String {
122-
let mut buffer = format!("class {}:", class.name);
122+
let mut buffer = String::new();
123+
for decorator in &class.decorators {
124+
buffer.push('@');
125+
imports.serialize_identifier(decorator, &mut buffer);
126+
buffer.push('\n');
127+
}
128+
buffer.push_str("class ");
129+
buffer.push_str(&class.name);
130+
buffer.push(':');
123131
if class.methods.is_empty() && class.attributes.is_empty() {
124132
buffer.push_str(" ...");
125133
return buffer;
@@ -433,6 +441,9 @@ impl ElementsUsedInAnnotations {
433441
}
434442

435443
fn walk_class(&mut self, class: &Class) {
444+
for decorator in &class.decorators {
445+
self.walk_identifier(decorator);
446+
}
436447
for method in &class.methods {
437448
self.walk_function(method);
438449
}
@@ -658,6 +669,10 @@ mod tests {
658669
name: "A".into(),
659670
methods: Vec::new(),
660671
attributes: Vec::new(),
672+
decorators: vec![PythonIdentifier {
673+
module: Some("typing".into()),
674+
name: "final".into(),
675+
}],
661676
}],
662677
functions: vec![Function {
663678
name: String::new(),
@@ -683,6 +698,7 @@ mod tests {
683698
"from bat import A as A2",
684699
"from builtins import int as int2",
685700
"from foo import A as A3, B",
701+
"from typing import final"
686702
]
687703
);
688704
let mut output = String::new();

pyo3-macros-backend/src/introspection.rs

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -89,19 +89,26 @@ pub fn class_introspection_code(
8989
pyo3_crate_path: &PyO3CratePath,
9090
ident: &Ident,
9191
name: &str,
92+
is_final: bool,
9293
) -> TokenStream {
93-
IntrospectionNode::Map(
94-
[
95-
("type", IntrospectionNode::String("class".into())),
96-
(
97-
"id",
98-
IntrospectionNode::IntrospectionId(Some(ident_to_type(ident))),
99-
),
100-
("name", IntrospectionNode::String(name.into())),
101-
]
102-
.into(),
103-
)
104-
.emit(pyo3_crate_path)
94+
let mut desc = HashMap::from([
95+
("type", IntrospectionNode::String("class".into())),
96+
(
97+
"id",
98+
IntrospectionNode::IntrospectionId(Some(ident_to_type(ident))),
99+
),
100+
("name", IntrospectionNode::String(name.into())),
101+
]);
102+
if is_final {
103+
desc.insert(
104+
"decorators",
105+
IntrospectionNode::List(vec![IntrospectionNode::ConstantType(
106+
PythonIdentifier::module_attr("typing", "final"),
107+
)
108+
.into()]),
109+
);
110+
}
111+
IntrospectionNode::Map(desc).emit(pyo3_crate_path)
105112
}
106113

107114
#[expect(clippy::too_many_arguments)]

pyo3-macros-backend/src/pyclass.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2683,7 +2683,12 @@ impl<'a> PyClassImplsBuilder<'a> {
26832683
let Ctx { pyo3_path, .. } = ctx;
26842684
let name = get_class_python_name(self.cls, self.attr).to_string();
26852685
let ident = self.cls;
2686-
let static_introspection = class_introspection_code(pyo3_path, ident, &name);
2686+
let static_introspection = class_introspection_code(
2687+
pyo3_path,
2688+
ident,
2689+
&name,
2690+
self.attr.options.subclass.is_none(),
2691+
);
26872692
let introspection_id = introspection_id_const();
26882693
quote! {
26892694
#static_introspection

pytests/src/lib.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ mod pyo3_pytests {
2525
#[pymodule_export]
2626
use {
2727
comparisons::comparisons, consts::consts, enums::enums, pyclasses::pyclasses,
28-
pyfunctions::pyfunctions,
28+
pyfunctions::pyfunctions, subclassing::subclassing,
2929
};
3030

3131
// Inserting to sys.modules allows importing submodules nicely from Python
@@ -43,7 +43,6 @@ mod pyo3_pytests {
4343
m.add_wrapped(wrap_pymodule!(othermod::othermod))?;
4444
m.add_wrapped(wrap_pymodule!(path::path))?;
4545
m.add_wrapped(wrap_pymodule!(sequence::sequence))?;
46-
m.add_wrapped(wrap_pymodule!(subclassing::subclassing))?;
4746

4847
// Inserting to sys.modules allows importing submodules nicely from Python
4948
// e.g. import pyo3_pytests.buf_and_str as bas

pytests/src/subclassing.rs

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,22 @@
22
33
use pyo3::prelude::*;
44

5-
#[pyclass(subclass)]
6-
pub struct Subclassable {}
5+
#[pymodule(gil_used = false)]
6+
pub mod subclassing {
7+
use pyo3::prelude::*;
78

8-
#[pymethods]
9-
impl Subclassable {
10-
#[new]
11-
fn new() -> Self {
12-
Subclassable {}
13-
}
9+
#[pyclass(subclass)]
10+
pub struct Subclassable {}
1411

15-
fn __str__(&self) -> &'static str {
16-
"Subclassable"
17-
}
18-
}
12+
#[pymethods]
13+
impl Subclassable {
14+
#[new]
15+
fn new() -> Self {
16+
Subclassable {}
17+
}
1918

20-
#[pymodule]
21-
pub fn subclassing(m: &Bound<'_, PyModule>) -> PyResult<()> {
22-
m.add_class::<Subclassable>()?;
23-
Ok(())
19+
fn __str__(&self) -> &'static str {
20+
"Subclassable"
21+
}
22+
}
2423
}

pytests/stubs/comparisons.pyi

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,23 @@
1+
from typing import final
2+
3+
@final
14
class Eq:
25
def __eq__(self, /, other: Eq) -> bool: ...
36
def __ne__(self, /, other: Eq) -> bool: ...
47
def __new__(cls, /, value: int) -> Eq: ...
58

9+
@final
610
class EqDefaultNe:
711
def __eq__(self, /, other: EqDefaultNe) -> bool: ...
812
def __new__(cls, /, value: int) -> EqDefaultNe: ...
913

14+
@final
1015
class EqDerived:
1116
def __eq__(self, /, other: EqDerived) -> bool: ...
1217
def __ne__(self, /, other: EqDerived) -> bool: ...
1318
def __new__(cls, /, value: int) -> EqDerived: ...
1419

20+
@final
1521
class Ordered:
1622
def __eq__(self, /, other: Ordered) -> bool: ...
1723
def __ge__(self, /, other: Ordered) -> bool: ...
@@ -21,6 +27,7 @@ class Ordered:
2127
def __ne__(self, /, other: Ordered) -> bool: ...
2228
def __new__(cls, /, value: int) -> Ordered: ...
2329

30+
@final
2431
class OrderedDefaultNe:
2532
def __eq__(self, /, other: OrderedDefaultNe) -> bool: ...
2633
def __ge__(self, /, other: OrderedDefaultNe) -> bool: ...
@@ -29,6 +36,7 @@ class OrderedDefaultNe:
2936
def __lt__(self, /, other: OrderedDefaultNe) -> bool: ...
3037
def __new__(cls, /, value: int) -> OrderedDefaultNe: ...
3138

39+
@final
3240
class OrderedDerived:
3341
def __eq__(self, /, other: OrderedDerived) -> bool: ...
3442
def __ge__(self, /, other: OrderedDerived) -> bool: ...
@@ -40,6 +48,7 @@ class OrderedDerived:
4048
def __new__(cls, /, value: int) -> OrderedDerived: ...
4149
def __str__(self, /) -> str: ...
4250

51+
@final
4352
class OrderedRichCmp:
4453
def __eq__(self, /, other: OrderedRichCmp) -> bool: ...
4554
def __ge__(self, /, other: OrderedRichCmp) -> bool: ...

pytests/stubs/consts.pyi

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
from typing import Final
1+
from typing import Final, final
22

33
PI: Final[float]
44
SIMPLE: Final = "SIMPLE"
55

6+
@final
67
class ClassWithConst:
78
INSTANCE: Final[ClassWithConst]

0 commit comments

Comments
 (0)