From 431f5e5ee64d529964b54298e9588e7d97fcc878 Mon Sep 17 00:00:00 2001 From: Thomas Pellissier-Tanon Date: Tue, 18 Nov 2025 13:15:57 +0100 Subject: [PATCH 1/2] Introspection: simplify code by leveraging that building type hint for containers now work --- newsfragments/5634.changed.md | 1 + pyo3-macros-backend/src/introspection.rs | 44 +++++------------------- src/conversions/std/option.rs | 8 +++++ src/impl_/extract_argument.rs | 5 +-- 4 files changed, 18 insertions(+), 40 deletions(-) create mode 100644 newsfragments/5634.changed.md diff --git a/newsfragments/5634.changed.md b/newsfragments/5634.changed.md new file mode 100644 index 00000000000..fed806191af --- /dev/null +++ b/newsfragments/5634.changed.md @@ -0,0 +1 @@ +Introspection: properly generate annotations for `Option` in both input and output. \ No newline at end of file diff --git a/pyo3-macros-backend/src/introspection.rs b/pyo3-macros-backend/src/introspection.rs index deac4c807d2..af70480f92a 100644 --- a/pyo3-macros-backend/src/introspection.rs +++ b/pyo3-macros-backend/src/introspection.rs @@ -339,34 +339,12 @@ 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 - if let Some(ty) = desc.option_wrapped_type { - // Special case to properly generate a `T | None` annotation - let mut ty = ty.clone(); - if let Some(class_type) = class_type { - replace_self(&mut ty, class_type); - } - elide_lifetimes(&mut ty); - params.insert( - "annotation", - IntrospectionNode::InputType { - rust_type: ty, - nullable: true, - }, - ); - } else { - 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 { - rust_type: ty, - nullable: false, - }, - ); + 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)); } IntrospectionNode::Map(params).into() } @@ -375,7 +353,7 @@ enum IntrospectionNode<'a> { String(Cow<'a, str>), Bool(bool), IntrospectionId(Option>), - InputType { rust_type: Type, nullable: bool }, + InputType(Type), OutputType { rust_type: Type, is_final: bool }, ConstantType(PythonIdentifier), Map(HashMap<&'static str, IntrospectionNode<'a>>), @@ -411,11 +389,8 @@ impl IntrospectionNode<'_> { }); content.push_str("\""); } - Self::InputType { - rust_type, - nullable, - } => { - let mut annotation = quote! { + 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")] @@ -424,9 +399,6 @@ impl IntrospectionNode<'_> { } >>::INPUT_TYPE }; - if nullable { - annotation = quote! { #pyo3_crate_path::inspect::TypeHint::union(&[#annotation, #pyo3_crate_path::inspect::TypeHint::builtin("None")]) }; - } content.push_tokens(serialize_type_hint(annotation, pyo3_crate_path)); } Self::OutputType { diff --git a/src/conversions/std/option.rs b/src/conversions/std/option.rs index 8cec88b4e6b..675ab4ff644 100644 --- a/src/conversions/std/option.rs +++ b/src/conversions/std/option.rs @@ -1,3 +1,5 @@ +#[cfg(feature = "experimental-inspect")] +use crate::inspect::TypeHint; use crate::{ conversion::IntoPyObject, types::any::PyAnyMethods, BoundObject, FromPyObject, PyAny, Python, }; @@ -10,6 +12,8 @@ where type Target = PyAny; type Output = Bound<'py, Self::Target>; type Error = T::Error; + #[cfg(feature = "experimental-inspect")] + const OUTPUT_TYPE: TypeHint = TypeHint::union(&[T::OUTPUT_TYPE, TypeHint::builtin("None")]); fn into_pyobject(self, py: Python<'py>) -> Result { self.map_or_else( @@ -30,6 +34,8 @@ where type Target = PyAny; type Output = Bound<'py, Self::Target>; type Error = <&'a T as IntoPyObject<'py>>::Error; + #[cfg(feature = "experimental-inspect")] + const OUTPUT_TYPE: TypeHint = as IntoPyObject<'_>>::OUTPUT_TYPE; #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { @@ -42,6 +48,8 @@ where T: FromPyObject<'a, 'py>, { type Error = T::Error; + #[cfg(feature = "experimental-inspect")] + const INPUT_TYPE: TypeHint = TypeHint::union(&[T::INPUT_TYPE, TypeHint::builtin("None")]); fn extract(obj: Borrowed<'a, 'py, PyAny>) -> Result { if obj.is_none() { diff --git a/src/impl_/extract_argument.rs b/src/impl_/extract_argument.rs index 8b5f94ad4dd..1188adf87e5 100644 --- a/src/impl_/extract_argument.rs +++ b/src/impl_/extract_argument.rs @@ -108,10 +108,7 @@ where type Error = T::Error; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: TypeHint = TypeHint::union(&[ - TypeHint::module_attr("typing", "Any"), - TypeHint::builtin("None"), - ]); + const INPUT_TYPE: TypeHint = TypeHint::union(&[T::INPUT_TYPE, TypeHint::builtin("None")]); #[inline] fn extract( From af23cc1a736c4856a9be4d1fd7f2db1a1695a6aa Mon Sep 17 00:00:00 2001 From: Thomas Tanon Date: Wed, 19 Nov 2025 10:28:03 +0100 Subject: [PATCH 2/2] Apply suggestion from @Tpt --- src/conversions/std/option.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/conversions/std/option.rs b/src/conversions/std/option.rs index 675ab4ff644..729fcc56668 100644 --- a/src/conversions/std/option.rs +++ b/src/conversions/std/option.rs @@ -35,7 +35,7 @@ where type Output = Bound<'py, Self::Target>; type Error = <&'a T as IntoPyObject<'py>>::Error; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: TypeHint = as IntoPyObject<'_>>::OUTPUT_TYPE; + const OUTPUT_TYPE: TypeHint = >::OUTPUT_TYPE; #[inline] fn into_pyobject(self, py: Python<'py>) -> Result {