diff --git a/pyo3-introspection/src/introspection.rs b/pyo3-introspection/src/introspection.rs index 42789941660..a2104937864 100644 --- a/pyo3-introspection/src/introspection.rs +++ b/pyo3-introspection/src/introspection.rs @@ -224,7 +224,7 @@ fn convert_decorator(decorator: &ChunkTypeHint) -> Result { if let TypeHintExpr::Identifier(i) = expr { Ok(i) } else { - bail!("PyO3 introspection currently only support decorators that are identifiers of a Python function") + bail!("PyO3 introspection currently only support decorators that are identifiers of a Python function, got {expr:?}") } } } diff --git a/pyo3-macros-backend/src/frompyobject.rs b/pyo3-macros-backend/src/frompyobject.rs index 841fcd2039f..b8c033cb1b3 100644 --- a/pyo3-macros-backend/src/frompyobject.rs +++ b/pyo3-macros-backend/src/frompyobject.rs @@ -1,7 +1,7 @@ use crate::attributes::{DefaultAttribute, FromPyWithAttribute, RenamingRule}; use crate::derive_attributes::{ContainerAttributes, FieldAttributes, FieldGetter}; #[cfg(feature = "experimental-inspect")] -use crate::introspection::elide_lifetimes; +use crate::type_hint::PythonTypeHint; use crate::utils::{self, Ctx}; use proc_macro2::TokenStream; use quote::{format_ident, quote, quote_spanned, ToTokens}; @@ -100,12 +100,8 @@ impl<'a> Enum<'a> { } #[cfg(feature = "experimental-inspect")] - fn input_type(&self, ctx: &Ctx) -> TokenStream { - let pyo3_crate_path = &ctx.pyo3_path; - let variants = self.variants.iter().map(|var| var.input_type(ctx)); - quote! { - #pyo3_crate_path::inspect::TypeHint::union(&[#(#variants),*]) - } + fn input_type(&self) -> PythonTypeHint { + PythonTypeHint::union(self.variants.iter().map(|var| var.input_type())) } } @@ -457,24 +453,23 @@ impl<'a> Container<'a> { } #[cfg(feature = "experimental-inspect")] - fn input_type(&self, ctx: &Ctx) -> TokenStream { - let pyo3_crate_path = &ctx.pyo3_path; + fn input_type(&self) -> PythonTypeHint { match &self.ty { ContainerType::StructNewtype(_, from_py_with, ty) => { - Self::field_input_type(from_py_with, ty, ctx) + Self::field_input_type(from_py_with, ty) } ContainerType::TupleNewtype(from_py_with, ty) => { - Self::field_input_type(from_py_with, ty, ctx) - } - ContainerType::Tuple(tups) => { - let elements = tups.iter().map(|TupleStructField { from_py_with, ty }| { - Self::field_input_type(from_py_with, ty, ctx) - }); - quote! { #pyo3_crate_path::inspect::TypeHint::subscript(&#pyo3_crate_path::inspect::TypeHint::builtin("tuple"), &[#(#elements),*]) } + Self::field_input_type(from_py_with, ty) } + ContainerType::Tuple(tups) => PythonTypeHint::subscript( + PythonTypeHint::builtin("tuple"), + tups.iter().map(|TupleStructField { from_py_with, ty }| { + Self::field_input_type(from_py_with, ty) + }), + ), ContainerType::Struct(_) => { // TODO: implement using a Protocol? - quote! { #pyo3_crate_path::inspect::TypeHint::module_attr("_typeshed", "Incomplete") } + PythonTypeHint::module_attr("_typeshed", "Incomplete") } } } @@ -483,16 +478,12 @@ impl<'a> Container<'a> { fn field_input_type( from_py_with: &Option, ty: &syn::Type, - ctx: &Ctx, - ) -> TokenStream { - let pyo3_crate_path = &ctx.pyo3_path; + ) -> PythonTypeHint { if from_py_with.is_some() { // We don't know what from_py_with is doing - quote! { #pyo3_crate_path::inspect::TypeHint::module_attr("_typeshed", "Incomplete") } + PythonTypeHint::module_attr("_typeshed", "Incomplete") } else { - let mut ty = ty.clone(); - elide_lifetimes(&mut ty); - quote! { <#ty as #pyo3_crate_path::FromPyObject<'_, '_>>::INPUT_TYPE } + PythonTypeHint::from_from_py_object(ty.clone(), None) } } } @@ -570,22 +561,22 @@ pub fn build_derive_from_pyobject(tokens: &DeriveInput) -> Result { .all(|p| matches!(p, syn::GenericParam::Lifetime(_))) { match &tokens.data { - syn::Data::Enum(en) => Enum::new(en, &tokens.ident, options)?.input_type(ctx), + syn::Data::Enum(en) => Enum::new(en, &tokens.ident, options)?.input_type(), syn::Data::Struct(st) => { let ident = &tokens.ident; - Container::new(&st.fields, parse_quote!(#ident), options.clone())? - .input_type(ctx) + Container::new(&st.fields, parse_quote!(#ident), options.clone())?.input_type() } syn::Data::Union(_) => { // Not supported at this point - quote! { #pyo3_crate_path::inspect::TypeHint::module_attr("_typeshed", "Incomplete") } + PythonTypeHint::module_attr("_typeshed", "Incomplete") } } } else { // We don't know how to deal with generic parameters // Blocked by https://github.com/rust-lang/rust/issues/76560 - quote! { #pyo3_crate_path::inspect::TypeHint::module_attr("_typeshed", "Incomplete") } - }; + PythonTypeHint::module_attr("_typeshed", "Incomplete") + } + .to_introspection_token_stream(pyo3_crate_path); quote! { const INPUT_TYPE: #pyo3_crate_path::inspect::TypeHint = #input_type; } }; #[cfg(not(feature = "experimental-inspect"))] diff --git a/pyo3-macros-backend/src/intopyobject.rs b/pyo3-macros-backend/src/intopyobject.rs index 75dfe054fa7..bd004cc6cd5 100644 --- a/pyo3-macros-backend/src/intopyobject.rs +++ b/pyo3-macros-backend/src/intopyobject.rs @@ -1,7 +1,7 @@ use crate::attributes::{IntoPyWithAttribute, RenamingRule}; use crate::derive_attributes::{ContainerAttributes, FieldAttributes}; #[cfg(feature = "experimental-inspect")] -use crate::introspection::elide_lifetimes; +use crate::type_hint::PythonTypeHint; use crate::utils::{self, Ctx}; use proc_macro2::{Span, TokenStream}; use quote::{format_ident, quote, quote_spanned, ToTokens}; @@ -360,26 +360,23 @@ impl<'a, const REF: bool> Container<'a, REF> { } #[cfg(feature = "experimental-inspect")] - fn output_type(&self, ctx: &Ctx) -> TokenStream { - let pyo3_crate_path = &ctx.pyo3_path; + fn output_type(&self) -> PythonTypeHint { match &self.ty { ContainerType::StructNewtype(field) | ContainerType::TupleNewtype(field) => { - Self::field_output_type(&None, &field.ty, ctx) + Self::field_output_type(&None, &field.ty) } - ContainerType::Tuple(tups) => { - let elements = tups.iter().map( + ContainerType::Tuple(tups) => PythonTypeHint::subscript( + PythonTypeHint::builtin("tuple"), + tups.iter().map( |TupleStructField { into_py_with, field, - }| { - Self::field_output_type(into_py_with, &field.ty, ctx) - }, - ); - quote! { #pyo3_crate_path::inspect::TypeHint::subscript(&#pyo3_crate_path::inspect::TypeHint::builtin("tuple"), &[#(#elements),*]) } - } + }| { Self::field_output_type(into_py_with, &field.ty) }, + ), + ), ContainerType::Struct(_) => { // TODO: implement using a Protocol? - quote! { #pyo3_crate_path::inspect::TypeHint::module_attr("_typeshed", "Incomplete") } + PythonTypeHint::module_attr("_typeshed", "Incomplete") } } } @@ -388,16 +385,12 @@ impl<'a, const REF: bool> Container<'a, REF> { fn field_output_type( into_py_with: &Option, ty: &syn::Type, - ctx: &Ctx, - ) -> TokenStream { - let pyo3_crate_path = &ctx.pyo3_path; + ) -> PythonTypeHint { if into_py_with.is_some() { // We don't know what into_py_with is doing - quote! { #pyo3_crate_path::inspect::TypeHint::module_attr("_typeshed", "Incomplete") } + PythonTypeHint::module_attr("_typeshed", "Incomplete") } else { - let mut ty = ty.clone(); - elide_lifetimes(&mut ty); - quote! { <#ty as #pyo3_crate_path::IntoPyObject<'_>>::OUTPUT_TYPE } + PythonTypeHint::from_into_py_object(ty.clone(), None) } } } @@ -477,12 +470,8 @@ impl<'a, const REF: bool> Enum<'a, REF> { } #[cfg(feature = "experimental-inspect")] - fn output_type(&self, ctx: &Ctx) -> TokenStream { - let pyo3_crate_path = &ctx.pyo3_path; - let variants = self.variants.iter().map(|var| var.output_type(ctx)); - quote! { - #pyo3_crate_path::inspect::TypeHint::union(&[#(#variants),*]) - } + fn output_type(&self) -> PythonTypeHint { + PythonTypeHint::union(self.variants.iter().map(|var| var.output_type())) } } @@ -586,7 +575,7 @@ pub fn build_derive_into_pyobject(tokens: &DeriveInput) -> Resu .all(|p| matches!(p, syn::GenericParam::Lifetime(_))) { match &tokens.data { - syn::Data::Enum(en) => Enum::::new(en, &tokens.ident)?.output_type(ctx), + syn::Data::Enum(en) => Enum::::new(en, &tokens.ident)?.output_type(), syn::Data::Struct(st) => { let ident = &tokens.ident; Container::::new( @@ -595,18 +584,19 @@ pub fn build_derive_into_pyobject(tokens: &DeriveInput) -> Resu parse_quote!(#ident), options, )? - .output_type(ctx) + .output_type() } syn::Data::Union(_) => { // Not supported at this point - quote! { #pyo3_crate_path::inspect::TypeHint::module_attr("_typeshed", "Incomplete") } + PythonTypeHint::module_attr("_typeshed", "Incomplete") } } } else { // We don't know how to deal with generic parameters // Blocked by https://github.com/rust-lang/rust/issues/76560 - quote! { #pyo3_crate_path::inspect::TypeHint::module_attr("_typeshed", "Incomplete") } - }; + PythonTypeHint::module_attr("_typeshed", "Incomplete") + } + .to_introspection_token_stream(pyo3_crate_path); quote! { const OUTPUT_TYPE: #pyo3_path::inspect::TypeHint = #output_type; } }; #[cfg(not(feature = "experimental-inspect"))] diff --git a/pyo3-macros-backend/src/introspection.rs b/pyo3-macros-backend/src/introspection.rs index af70480f92a..0deb0b3c349 100644 --- a/pyo3-macros-backend/src/introspection.rs +++ b/pyo3-macros-backend/src/introspection.rs @@ -10,6 +10,7 @@ use crate::method::{FnArg, RegularArg}; use crate::pyfunction::FunctionSignature; +use crate::type_hint::PythonTypeHint; use crate::utils::PyO3CratePath; use proc_macro2::{Span, TokenStream}; use quote::{format_ident, quote, ToTokens}; @@ -19,40 +20,10 @@ use std::collections::HashMap; 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, Lifetime, ReturnType, Type, TypePath}; +use syn::{Attribute, Ident, ReturnType, Type, TypePath}; static GLOBAL_COUNTER_FOR_UNIQUE_NAMES: AtomicUsize = AtomicUsize::new(0); -/// A Python type hint -pub struct PythonIdentifier { - module: Option<&'static str>, - name: Cow<'static, str>, -} - -impl PythonIdentifier { - pub fn builtins(name: impl Into>) -> Self { - Self { - module: Some("builtins"), - name: name.into(), - } - } - - pub fn local(name: impl Into>) -> Self { - Self { - module: None, - name: name.into(), - } - } - - pub fn module_attr(module: &'static str, name: impl Into>) -> Self { - Self { - module: Some(module), - name: name.into(), - } - } -} - pub fn module_introspection_code<'a>( pyo3_crate_path: &PyO3CratePath, name: &str, @@ -102,10 +73,7 @@ pub fn class_introspection_code( if is_final { desc.insert( "decorators", - IntrospectionNode::List(vec![IntrospectionNode::ConstantType( - PythonIdentifier::module_attr("typing", "final"), - ) - .into()]), + IntrospectionNode::List(vec![PythonTypeHint::module_attr("typing", "final").into()]), ); } IntrospectionNode::Map(desc).emit(pyo3_crate_path) @@ -119,7 +87,7 @@ pub fn function_introspection_code( signature: &FunctionSignature<'_>, first_argument: Option<&'static str>, returns: ReturnType, - decorators: impl IntoIterator, + decorators: impl IntoIterator, parent: Option<&Type>, ) -> TokenStream { let mut desc = HashMap::from([ @@ -139,24 +107,16 @@ pub fn function_introspection_code( IntrospectionNode::String(returns.to_python().into()) } else { match returns { - ReturnType::Default => PythonIdentifier::builtins("None").into(), + ReturnType::Default => PythonTypeHint::builtin("None"), ReturnType::Type(_, ty) => match *ty { Type::Tuple(t) if t.elems.is_empty() => { // () is converted to None in return types - PythonIdentifier::builtins("None").into() - } - mut ty => { - if let Some(class_type) = parent { - replace_self(&mut ty, class_type); - } - elide_lifetimes(&mut ty); - IntrospectionNode::OutputType { - rust_type: ty, - is_final: false, - } + PythonTypeHint::builtin("None") } + ty => PythonTypeHint::from_return_type(ty, parent), }, } + .into() }, ), ]); @@ -184,7 +144,7 @@ pub fn attribute_introspection_code( parent: Option<&Type>, name: String, value: String, - mut rust_type: Type, + rust_type: Type, is_final: bool, ) -> TokenStream { let mut desc = HashMap::from([ @@ -197,15 +157,16 @@ pub fn attribute_introspection_code( ]); if value == "..." { // We need to set a type, but not need to set the value to ..., all attributes have a value - if let Some(parent) = parent { - replace_self(&mut rust_type, parent); - } - elide_lifetimes(&mut rust_type); desc.insert( "annotation", - IntrospectionNode::OutputType { - rust_type, - is_final, + if is_final { + PythonTypeHint::subscript( + PythonTypeHint::module_attr("typing", "Final"), + [PythonTypeHint::from_return_type(rust_type, parent)], + ) + .into() + } else { + PythonTypeHint::from_return_type(rust_type, parent).into() }, ); } else { @@ -215,13 +176,11 @@ pub fn attribute_introspection_code( // Type checkers can infer the type from the value because it's typing.Literal[value] // So, following stubs best practices, we only write typing.Final and not // typing.Final[typing.literal[value]] - PythonIdentifier::module_attr("typing", "Final").into() + PythonTypeHint::module_attr("typing", "Final") } else { - IntrospectionNode::OutputType { - rust_type, - is_final, - } - }, + PythonTypeHint::from_return_type(rust_type, parent) + } + .into(), ); desc.insert("value", IntrospectionNode::String(value.into())); } @@ -339,12 +298,10 @@ fn argument_introspection_data<'a>( params.insert("annotation", IntrospectionNode::String(annotation.into())); } else if desc.from_py_with.is_none() { // If from_py_with is set we don't know anything on the input type - let mut ty = desc.ty.clone(); - if let Some(class_type) = class_type { - replace_self(&mut ty, class_type); - } - elide_lifetimes(&mut ty); - params.insert("annotation", IntrospectionNode::InputType(ty)); + params.insert( + "annotation", + PythonTypeHint::from_argument_type(desc.ty.clone(), class_type).into(), + ); } IntrospectionNode::Map(params).into() } @@ -353,9 +310,7 @@ enum IntrospectionNode<'a> { String(Cow<'a, str>), Bool(bool), IntrospectionId(Option>), - InputType(Type), - OutputType { rust_type: Type, is_final: bool }, - ConstantType(PythonIdentifier), + TypeHint(Cow<'a, PythonTypeHint>), Map(HashMap<&'static str, IntrospectionNode<'a>>), List(Vec>), } @@ -389,36 +344,11 @@ impl IntrospectionNode<'_> { }); content.push_str("\""); } - Self::InputType(rust_type) => { - let annotation = quote! { - <#rust_type as #pyo3_crate_path::impl_::extract_argument::PyFunctionArgument< - { - #[allow(unused_imports, reason = "`Probe` trait used on negative case only")] - use #pyo3_crate_path::impl_::pyclass::Probe as _; - #pyo3_crate_path::impl_::pyclass::IsFromPyObject::<#rust_type>::VALUE - } - >>::INPUT_TYPE - }; - content.push_tokens(serialize_type_hint(annotation, pyo3_crate_path)); - } - Self::OutputType { - rust_type, - is_final, - } => { - let mut annotation = quote! { <#rust_type as #pyo3_crate_path::impl_::introspection::PyReturnType>::OUTPUT_TYPE }; - if is_final { - annotation = quote! { #pyo3_crate_path::inspect::TypeHint::subscript(&#pyo3_crate_path::inspect::TypeHint::module_attr("typing", "Final"), &[#annotation]) }; - } - content.push_tokens(serialize_type_hint(annotation, pyo3_crate_path)); - } - Self::ConstantType(hint) => { - let name = &hint.name; - let annotation = if let Some(module) = &hint.module { - quote! { #pyo3_crate_path::inspect::TypeHint::module_attr(#module, #name) } - } else { - quote! { #pyo3_crate_path::inspect::TypeHint::local(#name) } - }; - content.push_tokens(serialize_type_hint(annotation, pyo3_crate_path)); + Self::TypeHint(hint) => { + content.push_tokens(serialize_type_hint( + hint.to_introspection_token_stream(pyo3_crate_path), + pyo3_crate_path, + )); } Self::Map(map) => { content.push_str("{"); @@ -459,9 +389,9 @@ impl IntrospectionNode<'_> { } } -impl From for IntrospectionNode<'static> { - fn from(element: PythonIdentifier) -> Self { - Self::ConstantType(element) +impl From for IntrospectionNode<'static> { + fn from(element: PythonTypeHint) -> Self { + Self::TypeHint(Cow::Owned(element)) } } @@ -492,8 +422,8 @@ impl<'a> From> for AttributedIntrospectionNode<'a> { } } -impl<'a> From for AttributedIntrospectionNode<'a> { - fn from(node: PythonIdentifier) -> Self { +impl<'a> From for AttributedIntrospectionNode<'a> { + fn from(node: PythonTypeHint) -> Self { IntrospectionNode::from(node).into() } } @@ -621,45 +551,3 @@ fn ident_to_type(ident: &Ident) -> Cow<'static, Type> { .into(), ) } - -/// Replaces all explicit lifetimes in `self` with elided (`'_`) lifetimes -/// -/// This is useful if `Self` is used in `const` context, where explicit -/// lifetimes are not allowed (yet). -pub fn elide_lifetimes(ty: &mut Type) { - struct ElideLifetimesVisitor; - - impl VisitMut for ElideLifetimesVisitor { - fn visit_lifetime_mut(&mut self, l: &mut syn::Lifetime) { - *l = Lifetime::new("'_", l.span()); - } - } - - ElideLifetimesVisitor.visit_type_mut(ty); -} - -// Replace Self in types with the given type -fn replace_self(ty: &mut Type, self_target: &Type) { - struct SelfReplacementVisitor<'a> { - self_target: &'a Type, - } - - impl VisitMut for SelfReplacementVisitor<'_> { - fn visit_type_mut(&mut self, ty: &mut Type) { - if let Type::Path(type_path) = ty { - if type_path.qself.is_none() - && type_path.path.segments.len() == 1 - && type_path.path.segments[0].ident == "Self" - && type_path.path.segments[0].arguments.is_empty() - { - // It is Self - *ty = self.self_target.clone(); - return; - } - } - visit_type_mut(self, ty); - } - } - - SelfReplacementVisitor { self_target }.visit_type_mut(ty); -} diff --git a/pyo3-macros-backend/src/lib.rs b/pyo3-macros-backend/src/lib.rs index 5a99f499f19..96510e1a8a5 100644 --- a/pyo3-macros-backend/src/lib.rs +++ b/pyo3-macros-backend/src/lib.rs @@ -25,6 +25,8 @@ mod pyimpl; mod pymethod; mod pyversions; mod quotes; +#[cfg(feature = "experimental-inspect")] +mod type_hint; pub use frompyobject::build_derive_from_pyobject; pub use intopyobject::build_derive_into_pyobject; diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 2d37d3cec7b..c53614622c0 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -16,7 +16,7 @@ use crate::attributes::{ use crate::combine_errors::CombineErrors; #[cfg(feature = "experimental-inspect")] use crate::introspection::{ - class_introspection_code, function_introspection_code, introspection_id_const, PythonIdentifier, + class_introspection_code, function_introspection_code, introspection_id_const, }; use crate::konst::{ConstAttributes, ConstSpec}; use crate::method::{FnArg, FnSpec, PyArg, RegularArg}; @@ -32,6 +32,8 @@ use crate::pymethod::{ __REPR__, __RICHCMP__, __STR__, }; use crate::pyversions::{is_abi3_before, is_py_before}; +#[cfg(feature = "experimental-inspect")] +use crate::type_hint::PythonTypeHint; use crate::utils::{self, apply_renaming_rule, Ctx, PythonDoc}; use crate::PyFunctionOptions; @@ -1897,7 +1899,7 @@ fn descriptors_to_items( &FunctionSignature::from_arguments(vec![]), Some("self"), parse_quote!(-> #return_type), - vec![PythonIdentifier::builtins("property")], + vec![PythonTypeHint::builtin("property")], Some(&parse_quote!(#cls)), )); } @@ -1936,7 +1938,7 @@ fn descriptors_to_items( })]), Some("self"), syn::ReturnType::Default, - vec![PythonIdentifier::local(format!("{name}.setter"))], + vec![PythonTypeHint::local(format!("{name}.setter"))], Some(&parse_quote!(#cls)), )); } diff --git a/pyo3-macros-backend/src/pyimpl.rs b/pyo3-macros-backend/src/pyimpl.rs index a5cf468546f..db30c0dcaab 100644 --- a/pyo3-macros-backend/src/pyimpl.rs +++ b/pyo3-macros-backend/src/pyimpl.rs @@ -2,12 +2,12 @@ use std::collections::HashSet; use crate::combine_errors::CombineErrors; #[cfg(feature = "experimental-inspect")] -use crate::introspection::{ - attribute_introspection_code, function_introspection_code, PythonIdentifier, -}; +use crate::introspection::{attribute_introspection_code, function_introspection_code}; #[cfg(feature = "experimental-inspect")] use crate::method::{FnSpec, FnType}; #[cfg(feature = "experimental-inspect")] +use crate::type_hint::PythonTypeHint; +#[cfg(feature = "experimental-inspect")] use crate::utils::expr_to_python; use crate::utils::{has_attribute, has_attribute_with_namespace, Ctx, PyO3CratePath}; use crate::{ @@ -401,11 +401,11 @@ fn method_introspection_code(spec: &FnSpec<'_>, parent: &syn::Type, ctx: &Ctx) - match &spec.tp { FnType::Getter(_) => { first_argument = Some("self"); - decorators.push(PythonIdentifier::builtins("property")); + decorators.push(PythonTypeHint::builtin("property")); } FnType::Setter(_) => { first_argument = Some("self"); - decorators.push(PythonIdentifier::local(format!("{name}.setter"))); + decorators.push(PythonTypeHint::local(format!("{name}.setter"))); } FnType::Fn(_) => { first_argument = Some("self"); @@ -414,12 +414,12 @@ fn method_introspection_code(spec: &FnSpec<'_>, parent: &syn::Type, ctx: &Ctx) - first_argument = Some("cls"); if spec.python_name != "__new__" { // special case __new__ - does not get the decorator - decorators.push(PythonIdentifier::builtins("classmethod")); + decorators.push(PythonTypeHint::builtin("classmethod")); } } FnType::FnStatic => { if spec.python_name != "__new__" { - decorators.push(PythonIdentifier::builtins("staticmethod")); + decorators.push(PythonTypeHint::builtin("staticmethod")); } else { // special case __new__ - does not get the decorator and gets first argument first_argument = Some("cls"); @@ -429,8 +429,8 @@ fn method_introspection_code(spec: &FnSpec<'_>, parent: &syn::Type, ctx: &Ctx) - FnType::ClassAttribute => { first_argument = Some("cls"); // TODO: this combination only works with Python 3.9-3.11 https://docs.python.org/3.11/library/functions.html#classmethod - decorators.push(PythonIdentifier::builtins("classmethod")); - decorators.push(PythonIdentifier::builtins("property")); + decorators.push(PythonTypeHint::builtin("classmethod")); + decorators.push(PythonTypeHint::builtin("property")); } } let return_type = if spec.python_name == "__new__" { diff --git a/pyo3-macros-backend/src/type_hint.rs b/pyo3-macros-backend/src/type_hint.rs new file mode 100644 index 00000000000..3cbe94ce721 --- /dev/null +++ b/pyo3-macros-backend/src/type_hint.rs @@ -0,0 +1,209 @@ +//! Define a data structure for Python type hints, mixing static data from macros and call to Pyo3 constants. + +use crate::utils::PyO3CratePath; +use proc_macro2::TokenStream; +use quote::quote; +use std::borrow::Cow; +use syn::visit_mut::{visit_type_mut, VisitMut}; +use syn::{Lifetime, Type}; + +#[derive(Clone)] +pub struct PythonTypeHint(PythonTypeHintVariant); + +#[derive(Clone)] +enum PythonTypeHintVariant { + /// The Python type hint of a FromPyObject implementation + FromPyObject(Type), + /// The Python type hint of a IntoPyObject implementation + IntoPyObject(Type), + /// The Python type matching the given Rust type given as a function argument + ArgumentType(Type), + /// The Python type matching the given Rust type given as a function returned value + ReturnType(Type), + /// A local type + Local(Cow<'static, str>), + /// A type in a module + ModuleAttribute { + module: Cow<'static, str>, + attr: Cow<'static, str>, + }, + /// A union + Union(Vec), + /// A subscript + Subscript { + value: Box, + slice: Vec, + }, +} + +impl PythonTypeHint { + /// Build from a local name + pub fn local(name: impl Into>) -> Self { + Self(PythonTypeHintVariant::Local(name.into())) + } + + /// Build from a builtins name like `None` + pub fn builtin(name: impl Into>) -> Self { + Self::module_attr("builtins", name) + } + + /// Build from a module and a name like `collections.abc` and `Sequence` + pub fn module_attr( + module: impl Into>, + name: impl Into>, + ) -> Self { + Self(PythonTypeHintVariant::ModuleAttribute { + module: module.into(), + attr: name.into(), + }) + } + + /// 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 + 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 + /// + /// If self_type is set, Self in the given type will be replaced by self_type + pub fn from_into_py_object(t: Type, self_type: Option<&Type>) -> Self { + Self(PythonTypeHintVariant::IntoPyObject(clean_type( + t, self_type, + ))) + } + + /// 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 + pub fn from_argument_type(t: Type, self_type: Option<&Type>) -> Self { + Self(PythonTypeHintVariant::ArgumentType(clean_type( + t, self_type, + ))) + } + + /// 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 + pub fn from_return_type(t: Type, self_type: Option<&Type>) -> Self { + Self(PythonTypeHintVariant::ReturnType(clean_type(t, self_type))) + } + + /// Build the union of the different element + pub fn union(elements: impl IntoIterator) -> Self { + let elements = elements.into_iter().collect::>(); + if elements.len() == 1 { + return elements.into_iter().next().unwrap(); + } + Self(PythonTypeHintVariant::Union(elements)) + } + + /// Build the subscripted type value[slice[0], ..., slice[n]] + pub fn subscript(value: Self, slice: impl IntoIterator) -> Self { + Self(crate::type_hint::PythonTypeHintVariant::Subscript { + value: Box::new(value), + slice: slice.into_iter().collect(), + }) + } + + pub fn to_introspection_token_stream(&self, pyo3_crate_path: &PyO3CratePath) -> TokenStream { + match &self.0 { + PythonTypeHintVariant::Local(name) => { + quote! { #pyo3_crate_path::inspect::TypeHint::local(#name) } + } + PythonTypeHintVariant::ModuleAttribute { module, attr } => { + if module == "builtins" { + quote! { #pyo3_crate_path::inspect::TypeHint::builtin(#attr) } + } else { + quote! { #pyo3_crate_path::inspect::TypeHint::module_attr(#module, #attr) } + } + } + PythonTypeHintVariant::FromPyObject(t) => { + quote! { <#t as #pyo3_crate_path::FromPyObject<'_, '_>>::INPUT_TYPE } + } + PythonTypeHintVariant::IntoPyObject(t) => { + quote! { <#t as #pyo3_crate_path::IntoPyObject<'_>>::OUTPUT_TYPE } + } + PythonTypeHintVariant::ArgumentType(t) => { + quote! { + <#t as #pyo3_crate_path::impl_::extract_argument::PyFunctionArgument< + { + #[allow(unused_imports, reason = "`Probe` trait used on negative case only")] + use #pyo3_crate_path::impl_::pyclass::Probe as _; + #pyo3_crate_path::impl_::pyclass::IsFromPyObject::<#t>::VALUE + } + >>::INPUT_TYPE + } + } + PythonTypeHintVariant::ReturnType(t) => { + quote! { <#t as #pyo3_crate_path::impl_::introspection::PyReturnType>::OUTPUT_TYPE } + } + PythonTypeHintVariant::Union(elements) => { + let elements = elements + .iter() + .map(|elt| elt.to_introspection_token_stream(pyo3_crate_path)); + quote! { #pyo3_crate_path::inspect::TypeHint::union(&[#(#elements),*]) } + } + PythonTypeHintVariant::Subscript { value, slice } => { + let value = value.to_introspection_token_stream(pyo3_crate_path); + let slice = slice + .iter() + .map(|elt| elt.to_introspection_token_stream(pyo3_crate_path)); + quote! { #pyo3_crate_path::inspect::TypeHint::subscript(&#value, &[#(#slice),*]) } + } + } + } +} + +fn clean_type(mut t: Type, self_type: Option<&Type>) -> Type { + if let Some(self_type) = self_type { + replace_self(&mut t, self_type); + } + elide_lifetimes(&mut t); + t +} + +/// Replaces all explicit lifetimes in `self` with elided (`'_`) lifetimes +/// +/// This is useful if `Self` is used in `const` context, where explicit +/// lifetimes are not allowed (yet). +fn elide_lifetimes(ty: &mut Type) { + struct ElideLifetimesVisitor; + + impl VisitMut for ElideLifetimesVisitor { + fn visit_lifetime_mut(&mut self, l: &mut syn::Lifetime) { + *l = Lifetime::new("'_", l.span()); + } + } + + ElideLifetimesVisitor.visit_type_mut(ty); +} + +// Replace Self in types with the given type +fn replace_self(ty: &mut Type, self_target: &Type) { + struct SelfReplacementVisitor<'a> { + self_target: &'a Type, + } + + impl VisitMut for SelfReplacementVisitor<'_> { + fn visit_type_mut(&mut self, ty: &mut Type) { + if let Type::Path(type_path) = ty { + if type_path.qself.is_none() + && type_path.path.segments.len() == 1 + && type_path.path.segments[0].ident == "Self" + && type_path.path.segments[0].arguments.is_empty() + { + // It is Self + *ty = self.self_target.clone(); + return; + } + } + visit_type_mut(self, ty); + } + } + + SelfReplacementVisitor { self_target }.visit_type_mut(ty); +}