Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions newsfragments/5331.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Introspection: emit base classes.
15 changes: 12 additions & 3 deletions pyo3-introspection/src/introspection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,10 +127,12 @@ fn convert_members<'a>(
Chunk::Class {
name,
id,
bases,
decorators,
} => classes.push(convert_class(
id,
name,
bases,
decorators,
chunks_by_id,
chunks_by_parent,
Expand Down Expand Up @@ -186,6 +188,7 @@ fn convert_members<'a>(
fn convert_class(
id: &str,
name: &str,
bases: &[ChunkTypeHint],
decorators: &[ChunkTypeHint],
chunks_by_id: &HashMap<&str, &Chunk>,
chunks_by_parent: &HashMap<&str, Vec<&Chunk>>,
Expand All @@ -205,16 +208,20 @@ fn convert_class(
);
Ok(Class {
name: name.into(),
bases: bases
.iter()
.map(convert_python_identifier)
.collect::<Result<_>>()?,
methods,
attributes,
decorators: decorators
.iter()
.map(convert_decorator)
.map(convert_python_identifier)
.collect::<Result<_>>()?,
})
}

fn convert_decorator(decorator: &ChunkTypeHint) -> Result<PythonIdentifier> {
fn convert_python_identifier(decorator: &ChunkTypeHint) -> Result<PythonIdentifier> {
match convert_type_hint(decorator) {
TypeHint::Plain(id) => Ok(PythonIdentifier {
module: None,
Expand All @@ -240,7 +247,7 @@ fn convert_function(
name: name.into(),
decorators: decorators
.iter()
.map(convert_decorator)
.map(convert_python_identifier)
.collect::<Result<_>>()?,
arguments: Arguments {
positional_only_arguments: arguments.posonlyargs.iter().map(convert_argument).collect(),
Expand Down Expand Up @@ -462,6 +469,8 @@ enum Chunk {
id: String,
name: String,
#[serde(default)]
bases: Vec<ChunkTypeHint>,
#[serde(default)]
decorators: Vec<ChunkTypeHint>,
},
Function {
Expand Down
1 change: 1 addition & 0 deletions pyo3-introspection/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pub struct Module {
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
pub struct Class {
pub name: String,
pub bases: Vec<PythonIdentifier>,
pub methods: Vec<Function>,
pub attributes: Vec<Attribute>,
/// decorator like 'typing.final'
Expand Down
17 changes: 17 additions & 0 deletions pyo3-introspection/src/stubs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,16 @@ fn class_stubs(class: &Class, imports: &Imports) -> String {
}
buffer.push_str("class ");
buffer.push_str(&class.name);
if !class.bases.is_empty() {
buffer.push('(');
for (i, base) in class.bases.iter().enumerate() {
if i > 0 {
buffer.push_str(", ");
}
imports.serialize_identifier(base, &mut buffer);
}
buffer.push(')');
}
buffer.push(':');
if class.methods.is_empty() && class.attributes.is_empty() {
buffer.push_str(" ...");
Expand Down Expand Up @@ -441,6 +451,9 @@ impl ElementsUsedInAnnotations {
}

fn walk_class(&mut self, class: &Class) {
for base in &class.bases {
self.walk_identifier(base);
}
for decorator in &class.decorators {
self.walk_identifier(decorator);
}
Expand Down Expand Up @@ -667,6 +680,10 @@ mod tests {
modules: Vec::new(),
classes: vec![Class {
name: "A".into(),
bases: vec![PythonIdentifier {
module: Some("builtins".into()),
name: "dict".into(),
}],
methods: Vec::new(),
attributes: Vec::new(),
decorators: vec![PythonIdentifier {
Expand Down
4 changes: 4 additions & 0 deletions pyo3-macros-backend/src/introspection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ pub fn class_introspection_code(
pyo3_crate_path: &PyO3CratePath,
ident: &Ident,
name: &str,
extends: Option<PythonTypeHint>,
is_final: bool,
) -> TokenStream {
let mut desc = HashMap::from([
Expand All @@ -70,6 +71,9 @@ pub fn class_introspection_code(
),
("name", IntrospectionNode::String(name.into())),
]);
if let Some(extends) = extends {
desc.insert("bases", IntrospectionNode::List(vec![extends.into()]));
}
if is_final {
desc.insert(
"decorators",
Expand Down
12 changes: 11 additions & 1 deletion pyo3-macros-backend/src/pyclass.rs
Original file line number Diff line number Diff line change
Expand Up @@ -429,7 +429,7 @@ impl FieldPyO3Options {
}
}

fn get_class_python_name<'a>(cls: &'a syn::Ident, args: &'a PyClassArgs) -> Cow<'a, syn::Ident> {
fn get_class_python_name<'a>(cls: &'a Ident, args: &'a PyClassArgs) -> Cow<'a, Ident> {
args.options
.name
.as_ref()
Expand Down Expand Up @@ -2689,6 +2689,16 @@ impl<'a> PyClassImplsBuilder<'a> {
pyo3_path,
ident,
&name,
self.attr.options.extends.as_ref().map(|attr| {
PythonTypeHint::from_type(
syn::TypePath {
qself: None,
path: attr.value.clone(),
}
.into(),
None,
)
}),
self.attr.options.subclass.is_none(),
);
let introspection_id = introspection_id_const();
Expand Down
24 changes: 18 additions & 6 deletions pyo3-macros-backend/src/type_hint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ enum PythonTypeHintVariant {
ArgumentType(Type),
/// The Python type matching the given Rust type given as a function returned value
ReturnType(Type),
/// The Python type matching the given Rust type
Type(Type),
/// A local type
Local(Cow<'static, str>),
/// A type in a module
Expand Down Expand Up @@ -58,18 +60,18 @@ impl PythonTypeHint {
})
}

/// The type hint of a FromPyObject implementation as a function argument
/// The type hint of a `FromPyObject` implementation as a function argument
///
/// If self_type is set, Self in the given type will be replaced by self_type
/// If self_type is set, self_type will replace Self in the given type
pub fn from_from_py_object(t: Type, self_type: Option<&Type>) -> Self {
Self(PythonTypeHintVariant::FromPyObject(clean_type(
t, self_type,
)))
}

/// The type hint of a IntoPyObject implementation as a function argument
/// The type hint of a `IntoPyObject` implementation as a function argument
///
/// If self_type is set, Self in the given type will be replaced by self_type
/// If self_type is set, self_type will replace Self in the given type
pub fn from_into_py_object(t: Type, self_type: Option<&Type>) -> Self {
Self(PythonTypeHintVariant::IntoPyObject(clean_type(
t, self_type,
Expand All @@ -78,7 +80,7 @@ impl PythonTypeHint {

/// The type hint of the Rust type used as a function argument
///
/// If self_type is set, Self in the given type will be replaced by self_type
/// If self_type is set, self_type will replace Self in the given type
pub fn from_argument_type(t: Type, self_type: Option<&Type>) -> Self {
Self(PythonTypeHintVariant::ArgumentType(clean_type(
t, self_type,
Expand All @@ -87,11 +89,18 @@ impl PythonTypeHint {

/// The type hint of the Rust type used as a function output type
///
/// If self_type is set, Self in the given type will be replaced by self_type
/// If self_type is set, self_type will replace Self in the given type
pub fn from_return_type(t: Type, self_type: Option<&Type>) -> Self {
Self(PythonTypeHintVariant::ReturnType(clean_type(t, self_type)))
}

/// The type hint of the Rust type `PyTypeCheck` trait.
///
/// If self_type is set, self_type will replace Self in the given type
pub fn from_type(t: Type, self_type: Option<&Type>) -> Self {
Self(PythonTypeHintVariant::Type(clean_type(t, self_type)))
}

/// Build the union of the different element
pub fn union(elements: impl IntoIterator<Item = Self>) -> Self {
let elements = elements.into_iter().collect::<Vec<_>>();
Expand Down Expand Up @@ -141,6 +150,9 @@ impl PythonTypeHint {
PythonTypeHintVariant::ReturnType(t) => {
quote! { <#t as #pyo3_crate_path::impl_::introspection::PyReturnType>::OUTPUT_TYPE }
}
PythonTypeHintVariant::Type(t) => {
quote! { <#t as #pyo3_crate_path::type_object::PyTypeCheck>::TYPE_HINT }
}
PythonTypeHintVariant::Union(elements) => {
let elements = elements
.iter()
Expand Down
34 changes: 34 additions & 0 deletions pytests/src/subclassing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ use pyo3::prelude::*;
#[pymodule(gil_used = false)]
pub mod subclassing {
use pyo3::prelude::*;
#[cfg(not(any(Py_LIMITED_API, GraalPy)))]
use pyo3::types::PyDict;

#[pyclass(subclass)]
pub struct Subclassable {}
Expand All @@ -20,4 +22,36 @@ pub mod subclassing {
"Subclassable"
}
}

#[pyclass(extends = Subclassable)]
pub struct Subclass {}

#[pymethods]
impl Subclass {
#[new]
fn new() -> (Self, Subclassable) {
(Subclass {}, Subclassable::new())
}

fn __str__(&self) -> &'static str {
"Subclass"
}
}

#[cfg(not(any(Py_LIMITED_API, GraalPy)))]
#[pyclass(extends = PyDict)]
pub struct SubDict {}

#[cfg(not(any(Py_LIMITED_API, GraalPy)))]
#[pymethods]
impl SubDict {
#[new]
fn new() -> Self {
Self {}
}

fn __str__(&self) -> &'static str {
"SubDict"
}
}
}
12 changes: 12 additions & 0 deletions pytests/stubs/subclassing.pyi
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
from typing import final

@final
class SubDict(dict):
def __new__(cls, /) -> SubDict: ...
def __str__(self, /) -> str: ...

@final
class Subclass(Subclassable):
def __new__(cls, /) -> Subclass: ...
def __str__(self, /) -> str: ...

class Subclassable:
def __new__(cls, /) -> Subclassable: ...
def __str__(self, /) -> str: ...
10 changes: 8 additions & 2 deletions pytests/tests/test_subclassing.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
from pyo3_pytests.subclassing import Subclassable
from pyo3_pytests.subclassing import Subclassable, Subclass


class SomeSubClass(Subclassable):
def __str__(self):
return "SomeSubclass"


def test_subclassing():
def test_python_subclassing():
a = SomeSubClass()
assert str(a) == "SomeSubclass"
assert type(a) is SomeSubClass


def test_rust_subclassing():
a = Subclass()
assert str(a) == "Subclass"
assert type(a) is Subclass
Loading