Skip to content
Draft
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.
14 changes: 11 additions & 3 deletions pyo3-introspection/src/introspection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,13 @@ fn convert_members<'a>(
chunks_by_parent,
)?);
}
Chunk::Class { name, id } => {
classes.push(convert_class(id, name, chunks_by_id, chunks_by_parent)?)
}
Chunk::Class { name, id, bases } => classes.push(convert_class(
id,
name,
bases,
chunks_by_id,
chunks_by_parent,
)?),
Chunk::Function {
name,
id: _,
Expand Down Expand Up @@ -164,6 +168,7 @@ fn convert_members<'a>(
fn convert_class(
id: &str,
name: &str,
bases: &[String],
chunks_by_id: &HashMap<&str, &Chunk>,
chunks_by_parent: &HashMap<&str, Vec<&Chunk>>,
) -> Result<Class> {
Expand All @@ -182,6 +187,7 @@ fn convert_class(
);
Ok(Class {
name: name.into(),
bases: bases.into(),
methods,
attributes,
})
Expand Down Expand Up @@ -404,6 +410,8 @@ enum Chunk {
Class {
id: String,
name: String,
#[serde(default)]
bases: Vec<String>,
},
Function {
#[serde(default)]
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<String>,
pub methods: Vec<Function>,
pub attributes: Vec<Attribute>,
}
Expand Down
13 changes: 12 additions & 1 deletion pyo3-introspection/src/stubs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,18 @@ fn module_stubs(module: &Module) -> String {
}

fn class_stubs(class: &Class, modules_to_import: &mut BTreeSet<String>) -> String {
let mut buffer = format!("class {}:", class.name);
let mut buffer = format!("class {}", class.name);
if !class.bases.is_empty() {
buffer.push('(');
for (i, base) in class.bases.iter().enumerate() {
if i > 0 {
buffer.push_str(", ");
}
buffer.push_str(annotation_stub(base, modules_to_import));
}
buffer.push(')');
}
buffer.push(':');
if class.methods.is_empty() && class.attributes.is_empty() {
buffer.push_str(" ...");
return buffer;
Expand Down
38 changes: 25 additions & 13 deletions pyo3-macros-backend/src/introspection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ use std::hash::{Hash, Hasher};
use std::mem::take;
use std::sync::atomic::{AtomicUsize, Ordering};
use syn::visit_mut::{visit_type_mut, VisitMut};
use syn::{Attribute, Ident, ReturnType, Type, TypePath};
use syn::{Attribute, Ident, Path, ReturnType, Type, TypePath};

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

Expand Down Expand Up @@ -61,19 +61,23 @@ pub fn class_introspection_code(
pyo3_crate_path: &PyO3CratePath,
ident: &Ident,
name: &str,
extends: Option<&Path>,
) -> TokenStream {
IntrospectionNode::Map(
[
("type", IntrospectionNode::String("class".into())),
(
"id",
IntrospectionNode::IntrospectionId(Some(ident_to_type(ident))),
),
("name", IntrospectionNode::String(name.into())),
]
.into(),
)
.emit(pyo3_crate_path)
let mut desc = HashMap::from([
("type", IntrospectionNode::String("class".into())),
(
"id",
IntrospectionNode::IntrospectionId(Some(ident_to_type(ident))),
),
("name", IntrospectionNode::String(name.into())),
]);
if let Some(extends) = extends {
desc.insert(
"bases",
IntrospectionNode::List(vec![IntrospectionNode::BaseType(extends).into()]),
);
}
IntrospectionNode::Map(desc).emit(pyo3_crate_path)
}

#[allow(clippy::too_many_arguments)]
Expand Down Expand Up @@ -345,6 +349,7 @@ enum IntrospectionNode<'a> {
IntrospectionId(Option<Cow<'a, Type>>),
InputType { rust_type: Type, nullable: bool },
OutputType { rust_type: Type, is_final: bool },
BaseType(&'a Path),
Map(HashMap<&'static str, IntrospectionNode<'a>>),
List(Vec<AttributedIntrospectionNode<'a>>),
}
Expand Down Expand Up @@ -410,6 +415,13 @@ impl IntrospectionNode<'_> {
}
content.push_str("\"");
}
Self::BaseType(path) => {
content.push_str("\"");
content.push_tokens(
quote! { <#path as #pyo3_crate_path::impl_::pyclass::PyClassBaseType>::BASE_NAME.as_bytes() },
);
content.push_str("\"");
}
Self::Map(map) => {
content.push_str("{");
for (i, (key, value)) in map.into_iter().enumerate() {
Expand Down
13 changes: 10 additions & 3 deletions pyo3-macros-backend/src/pyclass.rs
Original file line number Diff line number Diff line change
Expand Up @@ -399,7 +399,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 @@ -2352,12 +2352,14 @@ impl<'a> PyClassImplsBuilder<'a> {
};

let pyclass_base_type_impl = attr.options.subclass.map(|subclass| {
let name_with_module = get_class_python_module_and_name(cls, self.attr);
quote_spanned! { subclass.span() =>
impl #pyo3_path::impl_::pyclass::PyClassBaseType for #cls {
type LayoutAsBase = #pyo3_path::impl_::pycell::PyClassObject<Self>;
type BaseNativeType = <Self as #pyo3_path::impl_::pyclass::PyClassImpl>::BaseNativeType;
type Initializer = #pyo3_path::pyclass_init::PyClassInitializer<Self>;
type PyClassMutability = <Self as #pyo3_path::impl_::pyclass::PyClassImpl>::PyClassMutability;
const BASE_NAME: &'static str = #name_with_module;
}
}
});
Expand Down Expand Up @@ -2498,9 +2500,14 @@ impl<'a> PyClassImplsBuilder<'a> {
#[cfg(feature = "experimental-inspect")]
fn impl_introspection(&self, ctx: &Ctx) -> TokenStream {
let Ctx { pyo3_path, .. } = ctx;
let name = get_class_python_name(self.cls, self.attr).to_string();
let ident = self.cls;
let static_introspection = class_introspection_code(pyo3_path, ident, &name);
let name = get_class_python_name(ident, self.attr).to_string();
let static_introspection = class_introspection_code(
pyo3_path,
ident,
&name,
self.attr.options.extends.as_ref().map(|attr| &attr.value),
);
let introspection_id = introspection_id_const();
quote! {
#static_introspection
Expand Down
2 changes: 1 addition & 1 deletion pytests/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ mod pyo3_pytests {
#[pymodule_export]
use {
comparisons::comparisons, consts::consts, pyclasses::pyclasses, pyfunctions::pyfunctions,
subclassing::subclassing,
};

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

// Inserting to sys.modules allows importing submodules nicely from Python
// e.g. import pyo3_pytests.buf_and_str as bas
Expand Down
43 changes: 40 additions & 3 deletions pytests/src/subclassing.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
//! Test for [#220](https://github.com/PyO3/pyo3/issues/220)

use pyo3::prelude::*;
#[cfg(not(Py_LIMITED_API))]
use pyo3::types::PyDict;

#[pyclass(subclass)]
pub struct Subclassable {}
Expand All @@ -17,8 +19,43 @@ impl 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(Py_LIMITED_API))]
#[pyclass(extends = PyDict)]
pub struct SubDict {}

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

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

#[pymodule(gil_used = false)]
pub fn subclassing(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<Subclassable>()?;
Ok(())
pub mod subclassing {
#[cfg(not(Py_LIMITED_API))]
#[pymodule_export]
use super::SubDict;
#[pymodule_export]
use super::{Subclass, Subclassable};
}
11 changes: 11 additions & 0 deletions pytests/stubs/subclassing.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
class SubDict(dict):
def __new__(cls, /) -> None: ...
def __str__(self, /) -> str: ...

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

class Subclassable:
def __new__(cls, /) -> None: ...
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
24 changes: 24 additions & 0 deletions src/conversions/std/string.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ impl<'py> IntoPyObject<'py> for &str {
type Output = Bound<'py, Self::Target>;
type Error = Infallible;

#[cfg(feature = "experimental-inspect")]
const OUTPUT_TYPE: &'static str = String::OUTPUT_TYPE;

#[inline]
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
Ok(PyString::new(py, self))
Expand All @@ -30,6 +33,9 @@ impl<'py> IntoPyObject<'py> for &&str {
type Output = Bound<'py, Self::Target>;
type Error = Infallible;

#[cfg(feature = "experimental-inspect")]
const OUTPUT_TYPE: &'static str = String::OUTPUT_TYPE;

#[inline]
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
(*self).into_pyobject(py)
Expand All @@ -46,6 +52,9 @@ impl<'py> IntoPyObject<'py> for Cow<'_, str> {
type Output = Bound<'py, Self::Target>;
type Error = Infallible;

#[cfg(feature = "experimental-inspect")]
const OUTPUT_TYPE: &'static str = String::OUTPUT_TYPE;

#[inline]
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
(*self).into_pyobject(py)
Expand All @@ -62,6 +71,9 @@ impl<'py> IntoPyObject<'py> for &Cow<'_, str> {
type Output = Bound<'py, Self::Target>;
type Error = Infallible;

#[cfg(feature = "experimental-inspect")]
const OUTPUT_TYPE: &'static str = String::OUTPUT_TYPE;

#[inline]
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
(&**self).into_pyobject(py)
Expand All @@ -78,6 +90,9 @@ impl<'py> IntoPyObject<'py> for char {
type Output = Bound<'py, Self::Target>;
type Error = Infallible;

#[cfg(feature = "experimental-inspect")]
const OUTPUT_TYPE: &'static str = String::OUTPUT_TYPE;

fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
let mut bytes = [0u8; 4];
Ok(PyString::new(py, self.encode_utf8(&mut bytes)))
Expand All @@ -94,6 +109,9 @@ impl<'py> IntoPyObject<'py> for &char {
type Output = Bound<'py, Self::Target>;
type Error = Infallible;

#[cfg(feature = "experimental-inspect")]
const OUTPUT_TYPE: &'static str = String::OUTPUT_TYPE;

#[inline]
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
(*self).into_pyobject(py)
Expand All @@ -110,6 +128,9 @@ impl<'py> IntoPyObject<'py> for String {
type Output = Bound<'py, Self::Target>;
type Error = Infallible;

#[cfg(feature = "experimental-inspect")]
const OUTPUT_TYPE: &'static str = "str";

fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
Ok(PyString::new(py, &self))
}
Expand All @@ -125,6 +146,9 @@ impl<'py> IntoPyObject<'py> for &String {
type Output = Bound<'py, Self::Target>;
type Error = Infallible;

#[cfg(feature = "experimental-inspect")]
const OUTPUT_TYPE: &'static str = String::OUTPUT_TYPE;

#[inline]
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
Ok(PyString::new(py, self))
Expand Down
2 changes: 1 addition & 1 deletion src/exceptions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,7 @@ macro_rules! impl_native_exception (

$crate::impl_exception_boilerplate!($name);
$crate::pyobject_native_type!($name, $layout, |_py| unsafe { $crate::ffi::$exc_name as *mut $crate::ffi::PyTypeObject } $(, #checkfunction=$checkfunction)?);
$crate::pyobject_subclassable_native_type!($name, $layout);
$crate::pyobject_subclassable_native_type!($name, $layout, stringify!($name));
);
($name:ident, $exc_name:ident, $doc:expr) => (
impl_native_exception!($name, $exc_name, $doc, $crate::ffi::PyBaseExceptionObject);
Expand Down
2 changes: 2 additions & 0 deletions src/impl_/pyclass.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1139,6 +1139,8 @@ pub trait PyClassBaseType: Sized {
type BaseNativeType;
type Initializer: PyObjectInit<Self>;
type PyClassMutability: PyClassMutability;
/// Fully qualified name of the base class including modules
const BASE_NAME: &'static str;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have not gated this associated constants with the experimental-inspect feature because it's an internal trait often generated using macro_rules!.

I have not reused IntoPyObject::OUTPUT_TYPE because they might be different (typing.Any vs object...)

Happy to rename the constant to something better.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this information suitable to get from PyTypeInfo (which has name + module) ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe. I would need to fix the NAME for a lot of types. Currently it's setup by the macro to be the same as the Rust type name. So, for example, <PyDict as PyTypeInfo>::NAME == "PyDict" and not dict. Happy to do it if you want.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, that's a good point. I think that's probably just a historical mistake, it also leads to a lot of sad error messages where we leak the PyDict name out to consumers downstream that probably don't need to know about PyO3.

I think doing that as a precursor PR might be desirable? I can try to do so later, perhaps?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed. Thanks! It will actually be also useful to generate some type stubs. I hope to be able to do it today, feel free to focus on something else.

}

/// Implementation of tp_dealloc for pyclasses without gc
Expand Down
1 change: 1 addition & 0 deletions src/types/any.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ impl crate::impl_::pyclass::PyClassBaseType for PyAny {
type BaseNativeType = PyAny;
type Initializer = crate::impl_::pyclass_init::PyNativeTypeInitializer<Self>;
type PyClassMutability = crate::pycell::impl_::ImmutableClass;
const BASE_NAME: &'static str = "object";
}

/// This trait represents the Python APIs which are usable on all Python objects.
Expand Down
Loading
Loading