Skip to content

Commit 56efcdd

Browse files
authored
Basic introspection of #[derive(FromPyObject)] (#5339)
Struct fields are not supported yet
1 parent e93393c commit 56efcdd

File tree

9 files changed

+162
-15
lines changed

9 files changed

+162
-15
lines changed

newsfragments/5339.added.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Basic introspection of `#[derive(FromPyObject)]` (no struct fields support yet)

pyo3-macros-backend/src/derive_attributes.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ impl Parse for ContainerAttribute {
4444
}
4545
}
4646

47-
#[derive(Default)]
47+
#[derive(Default, Clone)]
4848
pub struct ContainerAttributes {
4949
/// Treat the Container as a Wrapper, operate directly on its field
5050
pub transparent: Option<attributes::kw::transparent>,

pyo3-macros-backend/src/frompyobject.rs

Lines changed: 108 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
use crate::attributes::{DefaultAttribute, FromPyWithAttribute, RenamingRule};
22
use crate::derive_attributes::{ContainerAttributes, FieldAttributes, FieldGetter};
3+
#[cfg(feature = "experimental-inspect")]
4+
use crate::introspection::ConcatenationBuilder;
5+
#[cfg(feature = "experimental-inspect")]
6+
use crate::utils::TypeExt;
37
use crate::utils::{self, Ctx};
48
use proc_macro2::TokenStream;
59
use quote::{format_ident, quote, quote_spanned, ToTokens};
@@ -96,17 +100,29 @@ impl<'a> Enum<'a> {
96100
)
97101
)
98102
}
103+
104+
#[cfg(feature = "experimental-inspect")]
105+
fn write_input_type(&self, builder: &mut ConcatenationBuilder, ctx: &Ctx) {
106+
for (i, var) in self.variants.iter().enumerate() {
107+
if i > 0 {
108+
builder.push_str(" | ");
109+
}
110+
var.write_input_type(builder, ctx);
111+
}
112+
}
99113
}
100114

101115
struct NamedStructField<'a> {
102116
ident: &'a syn::Ident,
103117
getter: Option<FieldGetter>,
104118
from_py_with: Option<FromPyWithAttribute>,
105119
default: Option<DefaultAttribute>,
120+
ty: &'a syn::Type,
106121
}
107122

108123
struct TupleStructField {
109124
from_py_with: Option<FromPyWithAttribute>,
125+
ty: syn::Type,
110126
}
111127

112128
/// Container Style
@@ -120,7 +136,8 @@ enum ContainerType<'a> {
120136
/// Newtype struct container, e.g. `#[transparent] struct Foo { a: String }`
121137
///
122138
/// The field specified by the identifier is extracted directly from the object.
123-
StructNewtype(&'a syn::Ident, Option<FromPyWithAttribute>),
139+
#[cfg_attr(not(feature = "experimental-inspect"), allow(unused))]
140+
StructNewtype(&'a syn::Ident, Option<FromPyWithAttribute>, &'a syn::Type),
124141
/// Tuple struct, e.g. `struct Foo(String)`.
125142
///
126143
/// Variant contains a list of conversion methods for each of the fields that are directly
@@ -129,7 +146,8 @@ enum ContainerType<'a> {
129146
/// Tuple newtype, e.g. `#[transparent] struct Foo(String)`
130147
///
131148
/// The wrapped field is directly extracted from the object.
132-
TupleNewtype(Option<FromPyWithAttribute>),
149+
#[cfg_attr(not(feature = "experimental-inspect"), allow(unused))]
150+
TupleNewtype(Option<FromPyWithAttribute>, Box<syn::Type>),
133151
}
134152

135153
/// Data container
@@ -168,6 +186,7 @@ impl<'a> Container<'a> {
168186
);
169187
Ok(TupleStructField {
170188
from_py_with: attrs.from_py_with,
189+
ty: field.ty.clone(),
171190
})
172191
})
173192
.collect::<Result<Vec<_>>>()?;
@@ -176,7 +195,7 @@ impl<'a> Container<'a> {
176195
// Always treat a 1-length tuple struct as "transparent", even without the
177196
// explicit annotation.
178197
let field = tuple_fields.pop().unwrap();
179-
ContainerType::TupleNewtype(field.from_py_with)
198+
ContainerType::TupleNewtype(field.from_py_with, Box::new(field.ty))
180199
} else if options.transparent.is_some() {
181200
bail_spanned!(
182201
fields.span() => "transparent structs and variants can only have 1 field"
@@ -216,6 +235,7 @@ impl<'a> Container<'a> {
216235
getter: attrs.getter,
217236
from_py_with: attrs.from_py_with,
218237
default: attrs.default,
238+
ty: &field.ty,
219239
})
220240
})
221241
.collect::<Result<Vec<_>>>()?;
@@ -237,7 +257,7 @@ impl<'a> Container<'a> {
237257
field.getter.is_none(),
238258
field.ident.span() => "`transparent` structs may not have a `getter` for the inner field"
239259
);
240-
ContainerType::StructNewtype(field.ident, field.from_py_with)
260+
ContainerType::StructNewtype(field.ident, field.from_py_with, field.ty)
241261
} else {
242262
ContainerType::Struct(struct_fields)
243263
}
@@ -274,10 +294,10 @@ impl<'a> Container<'a> {
274294
/// Build derivation body for a struct.
275295
fn build(&self, ctx: &Ctx) -> TokenStream {
276296
match &self.ty {
277-
ContainerType::StructNewtype(ident, from_py_with) => {
297+
ContainerType::StructNewtype(ident, from_py_with, _) => {
278298
self.build_newtype_struct(Some(ident), from_py_with, ctx)
279299
}
280-
ContainerType::TupleNewtype(from_py_with) => {
300+
ContainerType::TupleNewtype(from_py_with, _) => {
281301
self.build_newtype_struct(None, from_py_with, ctx)
282302
}
283303
ContainerType::Tuple(tups) => self.build_tuple_struct(tups, ctx),
@@ -438,6 +458,51 @@ impl<'a> Container<'a> {
438458

439459
quote!(::std::result::Result::Ok(#self_ty{#fields}))
440460
}
461+
462+
#[cfg(feature = "experimental-inspect")]
463+
fn write_input_type(&self, builder: &mut ConcatenationBuilder, ctx: &Ctx) {
464+
match &self.ty {
465+
ContainerType::StructNewtype(_, from_py_with, ty) => {
466+
Self::write_field_input_type(from_py_with, ty, builder, ctx);
467+
}
468+
ContainerType::TupleNewtype(from_py_with, ty) => {
469+
Self::write_field_input_type(from_py_with, ty, builder, ctx);
470+
}
471+
ContainerType::Tuple(tups) => {
472+
builder.push_str("tuple[");
473+
for (i, TupleStructField { from_py_with, ty }) in tups.iter().enumerate() {
474+
if i > 0 {
475+
builder.push_str(", ");
476+
}
477+
Self::write_field_input_type(from_py_with, ty, builder, ctx);
478+
}
479+
builder.push_str("]");
480+
}
481+
ContainerType::Struct(_) => {
482+
// TODO: implement using a Protocol?
483+
builder.push_str("_typeshed.Incomplete")
484+
}
485+
}
486+
}
487+
488+
#[cfg(feature = "experimental-inspect")]
489+
fn write_field_input_type(
490+
from_py_with: &Option<FromPyWithAttribute>,
491+
ty: &syn::Type,
492+
builder: &mut ConcatenationBuilder,
493+
ctx: &Ctx,
494+
) {
495+
if from_py_with.is_some() {
496+
// We don't know what from_py_with is doing
497+
builder.push_str("_typeshed.Incomplete")
498+
} else {
499+
let ty = ty.clone().elide_lifetimes();
500+
let pyo3_crate_path = &ctx.pyo3_path;
501+
builder.push_tokens(
502+
quote! { <#ty as #pyo3_crate_path::FromPyObject<'_>>::INPUT_TYPE.as_bytes() },
503+
)
504+
}
505+
}
441506
}
442507

443508
fn verify_and_get_lifetime(generics: &syn::Generics) -> Result<Option<&syn::LifetimeParam>> {
@@ -487,29 +552,64 @@ pub fn build_derive_from_pyobject(tokens: &DeriveInput) -> Result<TokenStream> {
487552
bail_spanned!(tokens.span() => "`transparent` or `annotation` is not supported \
488553
at top level for enums");
489554
}
490-
let en = Enum::new(en, &tokens.ident, options)?;
555+
let en = Enum::new(en, &tokens.ident, options.clone())?;
491556
en.build(ctx)
492557
}
493558
syn::Data::Struct(st) => {
494559
if let Some(lit_str) = &options.annotation {
495560
bail_spanned!(lit_str.span() => "`annotation` is unsupported for structs");
496561
}
497562
let ident = &tokens.ident;
498-
let st = Container::new(&st.fields, parse_quote!(#ident), options)?;
563+
let st = Container::new(&st.fields, parse_quote!(#ident), options.clone())?;
499564
st.build(ctx)
500565
}
501566
syn::Data::Union(_) => bail_spanned!(
502567
tokens.span() => "#[derive(FromPyObject)] is not supported for unions"
503568
),
504569
};
505570

571+
#[cfg(feature = "experimental-inspect")]
572+
let input_type = {
573+
let mut builder = ConcatenationBuilder::default();
574+
if tokens
575+
.generics
576+
.params
577+
.iter()
578+
.all(|p| matches!(p, syn::GenericParam::Lifetime(_)))
579+
{
580+
match &tokens.data {
581+
syn::Data::Enum(en) => {
582+
Enum::new(en, &tokens.ident, options)?.write_input_type(&mut builder, ctx)
583+
}
584+
syn::Data::Struct(st) => {
585+
let ident = &tokens.ident;
586+
Container::new(&st.fields, parse_quote!(#ident), options.clone())?
587+
.write_input_type(&mut builder, ctx)
588+
}
589+
syn::Data::Union(_) => {
590+
// Not supported at this point
591+
builder.push_str("_typeshed.Incomplete")
592+
}
593+
}
594+
} else {
595+
// We don't know how to deal with generic parameters
596+
// Blocked by https://github.com/rust-lang/rust/issues/76560
597+
builder.push_str("_typeshed.Incomplete")
598+
};
599+
let input_type = builder.into_token_stream(&ctx.pyo3_path);
600+
quote! { const INPUT_TYPE: &'static str = unsafe { ::std::str::from_utf8_unchecked(#input_type) }; }
601+
};
602+
#[cfg(not(feature = "experimental-inspect"))]
603+
let input_type = quote! {};
604+
506605
let ident = &tokens.ident;
507606
Ok(quote!(
508607
#[automatically_derived]
509608
impl #impl_generics #pyo3_path::FromPyObject<#lt_param> for #ident #ty_generics #where_clause {
510609
fn extract_bound(obj: &#pyo3_path::Bound<#lt_param, #pyo3_path::PyAny>) -> #pyo3_path::PyResult<Self> {
511610
#derives
512611
}
612+
#input_type
513613
}
514614
))
515615
}

pyo3-macros-backend/src/introspection.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -464,13 +464,13 @@ impl<'a> From<IntrospectionNode<'a>> for AttributedIntrospectionNode<'a> {
464464
}
465465

466466
#[derive(Default)]
467-
struct ConcatenationBuilder {
467+
pub struct ConcatenationBuilder {
468468
elements: Vec<ConcatenationBuilderElement>,
469469
current_string: String,
470470
}
471471

472472
impl ConcatenationBuilder {
473-
fn push_tokens(&mut self, token_stream: TokenStream) {
473+
pub fn push_tokens(&mut self, token_stream: TokenStream) {
474474
if !self.current_string.is_empty() {
475475
self.elements.push(ConcatenationBuilderElement::String(take(
476476
&mut self.current_string,
@@ -480,7 +480,7 @@ impl ConcatenationBuilder {
480480
.push(ConcatenationBuilderElement::TokenStream(token_stream));
481481
}
482482

483-
fn push_str(&mut self, value: &str) {
483+
pub fn push_str(&mut self, value: &str) {
484484
self.current_string.push_str(value);
485485
}
486486

@@ -502,7 +502,7 @@ impl ConcatenationBuilder {
502502
self.current_string.push('"');
503503
}
504504

505-
fn into_token_stream(self, pyo3_crate_path: &PyO3CratePath) -> TokenStream {
505+
pub fn into_token_stream(self, pyo3_crate_path: &PyO3CratePath) -> TokenStream {
506506
let mut elements = self.elements;
507507
if !self.current_string.is_empty() {
508508
elements.push(ConcatenationBuilderElement::String(self.current_string));

pyo3-macros-backend/src/pyclass.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2373,6 +2373,13 @@ impl<'a> PyClassImplsBuilder<'a> {
23732373
}
23742374
};
23752375

2376+
let type_name = if cfg!(feature = "experimental-inspect") {
2377+
let full_name = get_class_python_module_and_name(cls, self.attr);
2378+
quote! { const TYPE_NAME: &'static str = #full_name; }
2379+
} else {
2380+
quote! {}
2381+
};
2382+
23762383
Ok(quote! {
23772384
#assertions
23782385

@@ -2393,6 +2400,8 @@ impl<'a> PyClassImplsBuilder<'a> {
23932400
type WeakRef = #weakref;
23942401
type BaseNativeType = #base_nativetype;
23952402

2403+
#type_name
2404+
23962405
fn items_iter() -> #pyo3_path::impl_::pyclass::PyClassItemsIter {
23972406
use #pyo3_path::impl_::pyclass::*;
23982407
let collector = PyClassImplCollector::<Self>::new();

pytests/src/pyclasses.rs

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use pyo3::prelude::*;
55
use pyo3::types::PyType;
66

77
#[pyclass]
8+
#[derive(Clone, Default)]
89
struct EmptyClass {}
910

1011
#[pymethods]
@@ -104,6 +105,7 @@ impl ClassWithDict {
104105
}
105106

106107
#[pyclass]
108+
#[derive(Clone)]
107109
struct ClassWithDecorators {
108110
attr: usize,
109111
}
@@ -142,14 +144,32 @@ impl ClassWithDecorators {
142144
}
143145
}
144146

147+
#[derive(FromPyObject, IntoPyObject)]
148+
enum AClass {
149+
NewType(EmptyClass),
150+
Tuple(EmptyClass, EmptyClass),
151+
Struct {
152+
f: EmptyClass,
153+
#[pyo3(item(42))]
154+
g: EmptyClass,
155+
#[pyo3(default)]
156+
h: EmptyClass,
157+
},
158+
}
159+
160+
#[pyfunction]
161+
fn map_a_class(cls: AClass) -> AClass {
162+
cls
163+
}
164+
145165
#[pymodule(gil_used = false)]
146166
pub mod pyclasses {
147167
#[cfg(any(Py_3_10, not(Py_LIMITED_API)))]
148168
#[pymodule_export]
149169
use super::ClassWithDict;
150170
#[pymodule_export]
151171
use super::{
152-
AssertingBaseClass, ClassWithDecorators, ClassWithoutConstructor, EmptyClass, PyClassIter,
153-
PyClassThreadIter,
172+
map_a_class, AssertingBaseClass, ClassWithDecorators, ClassWithoutConstructor, EmptyClass,
173+
PyClassIter, PyClassThreadIter,
154174
};
155175
}

pytests/stubs/pyclasses.pyi

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import _typeshed
12
import typing
23

34
class AssertingBaseClass:
@@ -34,3 +35,7 @@ class PyClassIter:
3435
class PyClassThreadIter:
3536
def __new__(cls, /) -> None: ...
3637
def __next__(self, /) -> int: ...
38+
39+
def map_a_class(
40+
cls: EmptyClass | tuple[EmptyClass, EmptyClass] | _typeshed.Incomplete,
41+
) -> typing.Any: ...

src/conversion.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,9 @@ impl<T> FromPyObject<'_> for T
407407
where
408408
T: PyClass + Clone,
409409
{
410+
#[cfg(feature = "experimental-inspect")]
411+
const INPUT_TYPE: &'static str = <T as crate::impl_::pyclass::PyClassImpl>::TYPE_NAME;
412+
410413
fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult<Self> {
411414
let bound = obj.cast::<Self>()?;
412415
Ok(bound.try_borrow()?.clone())
@@ -417,6 +420,9 @@ impl<'py, T> FromPyObject<'py> for PyRef<'py, T>
417420
where
418421
T: PyClass,
419422
{
423+
#[cfg(feature = "experimental-inspect")]
424+
const INPUT_TYPE: &'static str = <T as crate::impl_::pyclass::PyClassImpl>::TYPE_NAME;
425+
420426
fn extract_bound(obj: &Bound<'py, PyAny>) -> PyResult<Self> {
421427
obj.cast::<T>()?.try_borrow().map_err(Into::into)
422428
}
@@ -426,6 +432,9 @@ impl<'py, T> FromPyObject<'py> for PyRefMut<'py, T>
426432
where
427433
T: PyClass<Frozen = False>,
428434
{
435+
#[cfg(feature = "experimental-inspect")]
436+
const INPUT_TYPE: &'static str = <T as crate::impl_::pyclass::PyClassImpl>::TYPE_NAME;
437+
429438
fn extract_bound(obj: &Bound<'py, PyAny>) -> PyResult<Self> {
430439
obj.cast::<T>()?.try_borrow_mut().map_err(Into::into)
431440
}

src/impl_/pyclass.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,9 @@ pub trait PyClassImpl: Sized + 'static {
219219
/// from the PyClassDocGenerator` type.
220220
const DOC: &'static CStr;
221221

222+
#[cfg(feature = "experimental-inspect")]
223+
const TYPE_NAME: &'static str;
224+
222225
fn items_iter() -> PyClassItemsIter;
223226

224227
#[inline]

0 commit comments

Comments
 (0)