Skip to content

Commit ae15fd0

Browse files
committed
Introspection: Basic pyclass(extends) support
Introduces PyClassBaseType::BASE_NAME to store the module name + class name of the base type
1 parent b788b9b commit ae15fd0

File tree

22 files changed

+171
-41
lines changed

22 files changed

+171
-41
lines changed

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: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,9 +120,13 @@ fn convert_members<'a>(
120120
chunks_by_parent,
121121
)?);
122122
}
123-
Chunk::Class { name, id } => {
124-
classes.push(convert_class(id, name, chunks_by_id, chunks_by_parent)?)
125-
}
123+
Chunk::Class { name, id, bases } => classes.push(convert_class(
124+
id,
125+
name,
126+
bases,
127+
chunks_by_id,
128+
chunks_by_parent,
129+
)?),
126130
Chunk::Function {
127131
name,
128132
id: _,
@@ -164,6 +168,7 @@ fn convert_members<'a>(
164168
fn convert_class(
165169
id: &str,
166170
name: &str,
171+
bases: &[String],
167172
chunks_by_id: &HashMap<&str, &Chunk>,
168173
chunks_by_parent: &HashMap<&str, Vec<&Chunk>>,
169174
) -> Result<Class> {
@@ -182,6 +187,7 @@ fn convert_class(
182187
);
183188
Ok(Class {
184189
name: name.into(),
190+
bases: bases.into(),
185191
methods,
186192
attributes,
187193
})
@@ -404,6 +410,8 @@ enum Chunk {
404410
Class {
405411
id: String,
406412
name: String,
413+
#[serde(default)]
414+
bases: Vec<String>,
407415
},
408416
Function {
409417
#[serde(default)]

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<String>,
1415
pub methods: Vec<Function>,
1516
pub attributes: Vec<Attribute>,
1617
}

pyo3-introspection/src/stubs.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,18 @@ fn module_stubs(module: &Module) -> String {
100100
}
101101

102102
fn class_stubs(class: &Class, modules_to_import: &mut BTreeSet<String>) -> String {
103-
let mut buffer = format!("class {}:", class.name);
103+
let mut buffer = format!("class {}", class.name);
104+
if !class.bases.is_empty() {
105+
buffer.push('(');
106+
for (i, base) in class.bases.iter().enumerate() {
107+
if i > 0 {
108+
buffer.push_str(", ");
109+
}
110+
buffer.push_str(annotation_stub(base, modules_to_import));
111+
}
112+
buffer.push(')');
113+
}
114+
buffer.push(':');
104115
if class.methods.is_empty() && class.attributes.is_empty() {
105116
buffer.push_str(" ...");
106117
return buffer;

pyo3-macros-backend/src/introspection.rs

Lines changed: 25 additions & 13 deletions
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, ReturnType, Type, TypePath};
23+
use syn::{Attribute, Ident, Path, ReturnType, Type, TypePath};
2424

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

@@ -61,19 +61,23 @@ pub fn class_introspection_code(
6161
pyo3_crate_path: &PyO3CratePath,
6262
ident: &Ident,
6363
name: &str,
64+
extends: Option<&Path>,
6465
) -> TokenStream {
65-
IntrospectionNode::Map(
66-
[
67-
("type", IntrospectionNode::String("class".into())),
68-
(
69-
"id",
70-
IntrospectionNode::IntrospectionId(Some(ident_to_type(ident))),
71-
),
72-
("name", IntrospectionNode::String(name.into())),
73-
]
74-
.into(),
75-
)
76-
.emit(pyo3_crate_path)
66+
let mut desc = HashMap::from([
67+
("type", IntrospectionNode::String("class".into())),
68+
(
69+
"id",
70+
IntrospectionNode::IntrospectionId(Some(ident_to_type(ident))),
71+
),
72+
("name", IntrospectionNode::String(name.into())),
73+
]);
74+
if let Some(extends) = extends {
75+
desc.insert(
76+
"bases",
77+
IntrospectionNode::List(vec![IntrospectionNode::BaseType(extends).into()]),
78+
);
79+
}
80+
IntrospectionNode::Map(desc).emit(pyo3_crate_path)
7781
}
7882

7983
#[allow(clippy::too_many_arguments)]
@@ -345,6 +349,7 @@ enum IntrospectionNode<'a> {
345349
IntrospectionId(Option<Cow<'a, Type>>),
346350
InputType { rust_type: Type, nullable: bool },
347351
OutputType { rust_type: Type, is_final: bool },
352+
BaseType(&'a Path),
348353
Map(HashMap<&'static str, IntrospectionNode<'a>>),
349354
List(Vec<AttributedIntrospectionNode<'a>>),
350355
}
@@ -410,6 +415,13 @@ impl IntrospectionNode<'_> {
410415
}
411416
content.push_str("\"");
412417
}
418+
Self::BaseType(path) => {
419+
content.push_str("\"");
420+
content.push_tokens(
421+
quote! { <#path as #pyo3_crate_path::impl_::pyclass::PyClassBaseType>::BASE_NAME.as_bytes() },
422+
);
423+
content.push_str("\"");
424+
}
413425
Self::Map(map) => {
414426
content.push_str("{");
415427
for (i, (key, value)) in map.into_iter().enumerate() {

pyo3-macros-backend/src/pyclass.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -399,7 +399,7 @@ impl FieldPyO3Options {
399399
}
400400
}
401401

402-
fn get_class_python_name<'a>(cls: &'a syn::Ident, args: &'a PyClassArgs) -> Cow<'a, syn::Ident> {
402+
fn get_class_python_name<'a>(cls: &'a Ident, args: &'a PyClassArgs) -> Cow<'a, Ident> {
403403
args.options
404404
.name
405405
.as_ref()
@@ -2352,12 +2352,14 @@ impl<'a> PyClassImplsBuilder<'a> {
23522352
};
23532353

23542354
let pyclass_base_type_impl = attr.options.subclass.map(|subclass| {
2355+
let name_with_module = get_class_python_module_and_name(cls, self.attr);
23552356
quote_spanned! { subclass.span() =>
23562357
impl #pyo3_path::impl_::pyclass::PyClassBaseType for #cls {
23572358
type LayoutAsBase = #pyo3_path::impl_::pycell::PyClassObject<Self>;
23582359
type BaseNativeType = <Self as #pyo3_path::impl_::pyclass::PyClassImpl>::BaseNativeType;
23592360
type Initializer = #pyo3_path::pyclass_init::PyClassInitializer<Self>;
23602361
type PyClassMutability = <Self as #pyo3_path::impl_::pyclass::PyClassImpl>::PyClassMutability;
2362+
const BASE_NAME: &'static str = #name_with_module;
23612363
}
23622364
}
23632365
});
@@ -2498,9 +2500,14 @@ impl<'a> PyClassImplsBuilder<'a> {
24982500
#[cfg(feature = "experimental-inspect")]
24992501
fn impl_introspection(&self, ctx: &Ctx) -> TokenStream {
25002502
let Ctx { pyo3_path, .. } = ctx;
2501-
let name = get_class_python_name(self.cls, self.attr).to_string();
25022503
let ident = self.cls;
2503-
let static_introspection = class_introspection_code(pyo3_path, ident, &name);
2504+
let name = get_class_python_name(ident, self.attr).to_string();
2505+
let static_introspection = class_introspection_code(
2506+
pyo3_path,
2507+
ident,
2508+
&name,
2509+
self.attr.options.extends.as_ref().map(|attr| &attr.value),
2510+
);
25042511
let introspection_id = introspection_id_const();
25052512
quote! {
25062513
#static_introspection

pytests/src/lib.rs

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

3031
// Inserting to sys.modules allows importing submodules nicely from Python
@@ -43,7 +44,6 @@ mod pyo3_pytests {
4344
m.add_wrapped(wrap_pymodule!(othermod::othermod))?;
4445
m.add_wrapped(wrap_pymodule!(path::path))?;
4546
m.add_wrapped(wrap_pymodule!(sequence::sequence))?;
46-
m.add_wrapped(wrap_pymodule!(subclassing::subclassing))?;
4747

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

pytests/src/subclassing.rs

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
//! Test for [#220](https://github.com/PyO3/pyo3/issues/220)
22
33
use pyo3::prelude::*;
4+
#[cfg(not(Py_LIMITED_API))]
5+
use pyo3::types::PyDict;
46

57
#[pyclass(subclass)]
68
pub struct Subclassable {}
@@ -17,8 +19,43 @@ impl Subclassable {
1719
}
1820
}
1921

22+
#[pyclass(extends = Subclassable)]
23+
pub struct Subclass {}
24+
25+
#[pymethods]
26+
impl Subclass {
27+
#[new]
28+
fn new() -> (Self, Subclassable) {
29+
(Subclass {}, Subclassable::new())
30+
}
31+
32+
fn __str__(&self) -> &'static str {
33+
"Subclass"
34+
}
35+
}
36+
37+
#[cfg(not(Py_LIMITED_API))]
38+
#[pyclass(extends = PyDict)]
39+
pub struct SubDict {}
40+
41+
#[cfg(not(Py_LIMITED_API))]
42+
#[pymethods]
43+
impl SubDict {
44+
#[new]
45+
fn new() -> Self {
46+
Self {}
47+
}
48+
49+
fn __str__(&self) -> &'static str {
50+
"SubDict"
51+
}
52+
}
53+
2054
#[pymodule(gil_used = false)]
21-
pub fn subclassing(m: &Bound<'_, PyModule>) -> PyResult<()> {
22-
m.add_class::<Subclassable>()?;
23-
Ok(())
55+
pub mod subclassing {
56+
#[cfg(not(Py_LIMITED_API))]
57+
#[pymodule_export]
58+
use super::SubDict;
59+
#[pymodule_export]
60+
use super::{Subclass, Subclassable};
2461
}

pytests/stubs/subclassing.pyi

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
class SubDict(dict):
2+
def __new__(cls, /) -> None: ...
3+
def __str__(self, /) -> str: ...
4+
5+
class Subclass(Subclassable):
6+
def __new__(cls, /) -> None: ...
7+
def __str__(self, /) -> str: ...
8+
9+
class Subclassable:
10+
def __new__(cls, /) -> None: ...
11+
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)