Skip to content

Commit cdca8cf

Browse files
committed
Introspection: pyclass(extends) support
1 parent e4f90c6 commit cdca8cf

9 files changed

Lines changed: 101 additions & 7 deletions

File tree

newsfragments/5331.added.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Introspection: emit base classes.

pyo3-introspection/src/introspection.rs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -127,10 +127,12 @@ fn convert_members<'a>(
127127
Chunk::Class {
128128
name,
129129
id,
130+
bases,
130131
decorators,
131132
} => classes.push(convert_class(
132133
id,
133134
name,
135+
bases,
134136
decorators,
135137
chunks_by_id,
136138
chunks_by_parent,
@@ -186,6 +188,7 @@ fn convert_members<'a>(
186188
fn convert_class(
187189
id: &str,
188190
name: &str,
191+
bases: &[ChunkTypeHint],
189192
decorators: &[ChunkTypeHint],
190193
chunks_by_id: &HashMap<&str, &Chunk>,
191194
chunks_by_parent: &HashMap<&str, Vec<&Chunk>>,
@@ -205,16 +208,20 @@ fn convert_class(
205208
);
206209
Ok(Class {
207210
name: name.into(),
211+
bases: bases
212+
.iter()
213+
.map(convert_python_identifier)
214+
.collect::<Result<_>>()?,
208215
methods,
209216
attributes,
210217
decorators: decorators
211218
.iter()
212-
.map(convert_decorator)
219+
.map(convert_python_identifier)
213220
.collect::<Result<_>>()?,
214221
})
215222
}
216223

217-
fn convert_decorator(decorator: &ChunkTypeHint) -> Result<PythonIdentifier> {
224+
fn convert_python_identifier(decorator: &ChunkTypeHint) -> Result<PythonIdentifier> {
218225
match convert_type_hint(decorator) {
219226
TypeHint::Plain(id) => Ok(PythonIdentifier {
220227
module: None,
@@ -240,7 +247,7 @@ fn convert_function(
240247
name: name.into(),
241248
decorators: decorators
242249
.iter()
243-
.map(convert_decorator)
250+
.map(convert_python_identifier)
244251
.collect::<Result<_>>()?,
245252
arguments: Arguments {
246253
positional_only_arguments: arguments.posonlyargs.iter().map(convert_argument).collect(),
@@ -462,6 +469,8 @@ enum Chunk {
462469
id: String,
463470
name: String,
464471
#[serde(default)]
472+
bases: Vec<ChunkTypeHint>,
473+
#[serde(default)]
465474
decorators: Vec<ChunkTypeHint>,
466475
},
467476
Function {

pyo3-introspection/src/model.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ pub struct Module {
1111
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
1212
pub struct Class {
1313
pub name: String,
14+
pub bases: Vec<PythonIdentifier>,
1415
pub methods: Vec<Function>,
1516
pub attributes: Vec<Attribute>,
1617
/// decorator like 'typing.final'

pyo3-introspection/src/stubs.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,16 @@ fn class_stubs(class: &Class, imports: &Imports) -> String {
127127
}
128128
buffer.push_str("class ");
129129
buffer.push_str(&class.name);
130+
if !class.bases.is_empty() {
131+
buffer.push('(');
132+
for (i, base) in class.bases.iter().enumerate() {
133+
if i > 0 {
134+
buffer.push_str(", ");
135+
}
136+
imports.serialize_identifier(base, &mut buffer);
137+
}
138+
buffer.push(')');
139+
}
130140
buffer.push(':');
131141
if class.methods.is_empty() && class.attributes.is_empty() {
132142
buffer.push_str(" ...");
@@ -441,6 +451,9 @@ impl ElementsUsedInAnnotations {
441451
}
442452

443453
fn walk_class(&mut self, class: &Class) {
454+
for base in &class.bases {
455+
self.walk_identifier(base);
456+
}
444457
for decorator in &class.decorators {
445458
self.walk_identifier(decorator);
446459
}
@@ -667,6 +680,10 @@ mod tests {
667680
modules: Vec::new(),
668681
classes: vec![Class {
669682
name: "A".into(),
683+
bases: vec![PythonIdentifier {
684+
module: Some("builtins".into()),
685+
name: "dict".into(),
686+
}],
670687
methods: Vec::new(),
671688
attributes: Vec::new(),
672689
decorators: vec![PythonIdentifier {

pyo3-macros-backend/src/introspection.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ use std::hash::{Hash, Hasher};
2020
use std::mem::take;
2121
use std::sync::atomic::{AtomicUsize, Ordering};
2222
use syn::visit_mut::{visit_type_mut, VisitMut};
23-
use syn::{Attribute, Ident, Lifetime, ReturnType, Type, TypePath};
23+
use syn::{Attribute, Ident, Lifetime, Path, ReturnType, Type, TypePath};
2424

2525
static GLOBAL_COUNTER_FOR_UNIQUE_NAMES: AtomicUsize = AtomicUsize::new(0);
2626

@@ -89,6 +89,7 @@ pub fn class_introspection_code(
8989
pyo3_crate_path: &PyO3CratePath,
9090
ident: &Ident,
9191
name: &str,
92+
extends: Option<&Path>,
9293
is_final: bool,
9394
) -> TokenStream {
9495
let mut desc = HashMap::from([
@@ -99,6 +100,12 @@ pub fn class_introspection_code(
99100
),
100101
("name", IntrospectionNode::String(name.into())),
101102
]);
103+
if let Some(extends) = extends {
104+
desc.insert(
105+
"bases",
106+
IntrospectionNode::List(vec![IntrospectionNode::BaseType(extends).into()]),
107+
);
108+
}
102109
if is_final {
103110
desc.insert(
104111
"decorators",
@@ -355,6 +362,7 @@ enum IntrospectionNode<'a> {
355362
IntrospectionId(Option<Cow<'a, Type>>),
356363
InputType(Type),
357364
OutputType { rust_type: Type, is_final: bool },
365+
BaseType(&'a Path),
358366
ConstantType(PythonIdentifier),
359367
Map(HashMap<&'static str, IntrospectionNode<'a>>),
360368
List(Vec<AttributedIntrospectionNode<'a>>),
@@ -411,6 +419,11 @@ impl IntrospectionNode<'_> {
411419
}
412420
content.push_tokens(serialize_type_hint(annotation, pyo3_crate_path));
413421
}
422+
Self::BaseType(rust_type) => {
423+
let annotation =
424+
quote! { <#rust_type as #pyo3_crate_path::type_object::PyTypeInfo>::TYPE_HINT };
425+
content.push_tokens(serialize_type_hint(annotation, pyo3_crate_path));
426+
}
414427
Self::ConstantType(hint) => {
415428
let name = &hint.name;
416429
let annotation = if let Some(module) = &hint.module {

pyo3-macros-backend/src/pyclass.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -427,7 +427,7 @@ impl FieldPyO3Options {
427427
}
428428
}
429429

430-
fn get_class_python_name<'a>(cls: &'a syn::Ident, args: &'a PyClassArgs) -> Cow<'a, syn::Ident> {
430+
fn get_class_python_name<'a>(cls: &'a Ident, args: &'a PyClassArgs) -> Cow<'a, Ident> {
431431
args.options
432432
.name
433433
.as_ref()
@@ -2687,6 +2687,7 @@ impl<'a> PyClassImplsBuilder<'a> {
26872687
pyo3_path,
26882688
ident,
26892689
&name,
2690+
self.attr.options.extends.as_ref().map(|attr| &attr.value),
26902691
self.attr.options.subclass.is_none(),
26912692
);
26922693
let introspection_id = introspection_id_const();

pytests/src/subclassing.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ use pyo3::prelude::*;
55
#[pymodule(gil_used = false)]
66
pub mod subclassing {
77
use pyo3::prelude::*;
8+
#[cfg(not(Py_LIMITED_API))]
9+
use pyo3::types::PyDict;
810

911
#[pyclass(subclass)]
1012
pub struct Subclassable {}
@@ -20,4 +22,36 @@ pub mod subclassing {
2022
"Subclassable"
2123
}
2224
}
25+
26+
#[pyclass(extends = Subclassable)]
27+
pub struct Subclass {}
28+
29+
#[pymethods]
30+
impl Subclass {
31+
#[new]
32+
fn new() -> (Self, Subclassable) {
33+
(Subclass {}, Subclassable::new())
34+
}
35+
36+
fn __str__(&self) -> &'static str {
37+
"Subclass"
38+
}
39+
}
40+
41+
#[cfg(not(Py_LIMITED_API))]
42+
#[pyclass(extends = PyDict)]
43+
pub struct SubDict {}
44+
45+
#[cfg(not(Py_LIMITED_API))]
46+
#[pymethods]
47+
impl SubDict {
48+
#[new]
49+
fn new() -> Self {
50+
Self {}
51+
}
52+
53+
fn __str__(&self) -> &'static str {
54+
"SubDict"
55+
}
56+
}
2357
}

pytests/stubs/subclassing.pyi

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,15 @@
1+
from typing import final
2+
3+
@final
4+
class SubDict(dict):
5+
def __new__(cls, /) -> SubDict: ...
6+
def __str__(self, /) -> str: ...
7+
8+
@final
9+
class Subclass(Subclassable):
10+
def __new__(cls, /) -> Subclass: ...
11+
def __str__(self, /) -> str: ...
12+
113
class Subclassable:
214
def __new__(cls, /) -> Subclassable: ...
315
def __str__(self, /) -> str: ...

pytests/tests/test_subclassing.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
1-
from pyo3_pytests.subclassing import Subclassable
1+
from pyo3_pytests.subclassing import Subclassable, Subclass
22

33

44
class SomeSubClass(Subclassable):
55
def __str__(self):
66
return "SomeSubclass"
77

88

9-
def test_subclassing():
9+
def test_python_subclassing():
1010
a = SomeSubClass()
1111
assert str(a) == "SomeSubclass"
1212
assert type(a) is SomeSubClass
13+
14+
15+
def test_rust_subclassing():
16+
a = Subclass()
17+
assert str(a) == "Subclass"
18+
assert type(a) is Subclass

0 commit comments

Comments
 (0)