diff --git a/guide/src/class.md b/guide/src/class.md index 8772e857d7e..244b0f94b65 100644 --- a/guide/src/class.md +++ b/guide/src/class.md @@ -1378,6 +1378,7 @@ impl pyo3::types::DerefToPyAny for MyClass {} unsafe impl pyo3::type_object::PyTypeInfo for MyClass { const NAME: &'static str = "MyClass"; const MODULE: ::std::option::Option<&'static str> = ::std::option::Option::None; + #[inline] fn type_object_raw(py: pyo3::Python<'_>) -> *mut pyo3::ffi::PyTypeObject { ::lazy_type_object() @@ -1393,6 +1394,8 @@ impl pyo3::PyClass for MyClass { impl<'a, 'py> pyo3::impl_::extract_argument::PyFunctionArgument<'a, 'py, false> for &'a MyClass { type Holder = ::std::option::Option>; + #[cfg(feature = "experimental-inspect")] + const INPUT_TYPE: &'static str = "MyClass"; #[inline] fn extract(obj: &'a pyo3::Bound<'py, PyAny>, holder: &'a mut Self::Holder) -> pyo3::PyResult { @@ -1403,6 +1406,8 @@ impl<'a, 'py> pyo3::impl_::extract_argument::PyFunctionArgument<'a, 'py, false> impl<'a, 'py> pyo3::impl_::extract_argument::PyFunctionArgument<'a, 'py, false> for &'a mut MyClass { type Holder = ::std::option::Option>; + #[cfg(feature = "experimental-inspect")] + const INPUT_TYPE: &'static str = "MyClass"; #[inline] fn extract(obj: &'a pyo3::Bound<'py, PyAny>, holder: &'a mut Self::Holder) -> pyo3::PyResult { diff --git a/noxfile.py b/noxfile.py index 94e0f392044..68949340f97 100644 --- a/noxfile.py +++ b/noxfile.py @@ -865,6 +865,7 @@ def update_ui_tests(session: nox.Session): @nox.session(name="test-introspection") def test_introspection(session: nox.Session): session.install("maturin") + session.install("ruff") target = os.environ.get("CARGO_BUILD_TARGET") for options in ([], ["--release"]): if target is not None: diff --git a/pyo3-introspection/Cargo.toml b/pyo3-introspection/Cargo.toml index d861ec7b6d5..344e0e633e8 100644 --- a/pyo3-introspection/Cargo.toml +++ b/pyo3-introspection/Cargo.toml @@ -14,5 +14,8 @@ goblin = "0.9.0" serde = { version = "1", features = ["derive"] } serde_json = "1" +[dev-dependencies] +tempfile = "3.12.0" + [lints] workspace = true diff --git a/pyo3-introspection/src/introspection.rs b/pyo3-introspection/src/introspection.rs index e4f49d5e0e3..d4c63eb853a 100644 --- a/pyo3-introspection/src/introspection.rs +++ b/pyo3-introspection/src/introspection.rs @@ -112,6 +112,7 @@ fn convert_argument(arg: &ChunkArgument) -> Argument { Argument { name: arg.name.clone(), default_value: arg.default.clone(), + annotation: arg.annotation.clone(), } } @@ -315,4 +316,6 @@ struct ChunkArgument { name: String, #[serde(default)] default: Option, + #[serde(default)] + annotation: Option, } diff --git a/pyo3-introspection/src/model.rs b/pyo3-introspection/src/model.rs index 7705a0006a4..c6037dc15f3 100644 --- a/pyo3-introspection/src/model.rs +++ b/pyo3-introspection/src/model.rs @@ -36,6 +36,8 @@ pub struct Argument { pub name: String, /// Default value as a Python expression pub default_value: Option, + /// Type annotation as a Python expression + pub annotation: Option, } /// A variable length argument ie. *vararg or **kwarg diff --git a/pyo3-introspection/src/stubs.rs b/pyo3-introspection/src/stubs.rs index 2312d7d37ac..88163c1eefe 100644 --- a/pyo3-introspection/src/stubs.rs +++ b/pyo3-introspection/src/stubs.rs @@ -1,5 +1,5 @@ use crate::model::{Argument, Class, Function, Module, VariableLengthArgument}; -use std::collections::HashMap; +use std::collections::{BTreeSet, HashMap}; use std::path::{Path, PathBuf}; /// Generates the [type stubs](https://typing.readthedocs.io/en/latest/source/stubs.html) of a given module. @@ -32,32 +32,39 @@ fn add_module_stub_files( /// Generates the module stubs to a String, not including submodules fn module_stubs(module: &Module) -> String { + let mut modules_to_import = BTreeSet::new(); let mut elements = Vec::new(); for class in &module.classes { elements.push(class_stubs(class)); } for function in &module.functions { - elements.push(function_stubs(function)); + elements.push(function_stubs(function, &mut modules_to_import)); } elements.push(String::new()); // last line jump - elements.join("\n") + + let mut final_elements = Vec::new(); + for module_to_import in &modules_to_import { + final_elements.push(format!("import {module_to_import}")); + } + final_elements.extend(elements); + final_elements.join("\n") } fn class_stubs(class: &Class) -> String { format!("class {}: ...", class.name) } -fn function_stubs(function: &Function) -> String { +fn function_stubs(function: &Function, modules_to_import: &mut BTreeSet) -> String { // Signature let mut parameters = Vec::new(); for argument in &function.arguments.positional_only_arguments { - parameters.push(argument_stub(argument)); + parameters.push(argument_stub(argument, modules_to_import)); } if !function.arguments.positional_only_arguments.is_empty() { parameters.push("/".into()); } for argument in &function.arguments.arguments { - parameters.push(argument_stub(argument)); + parameters.push(argument_stub(argument, modules_to_import)); } if let Some(argument) = &function.arguments.vararg { parameters.push(format!("*{}", variable_length_argument_stub(argument))); @@ -65,7 +72,7 @@ fn function_stubs(function: &Function) -> String { parameters.push("*".into()); } for argument in &function.arguments.keyword_only_arguments { - parameters.push(argument_stub(argument)); + parameters.push(argument_stub(argument, modules_to_import)); } if let Some(argument) = &function.arguments.kwarg { parameters.push(format!("**{}", variable_length_argument_stub(argument))); @@ -73,10 +80,22 @@ fn function_stubs(function: &Function) -> String { format!("def {}({}): ...", function.name, parameters.join(", ")) } -fn argument_stub(argument: &Argument) -> String { +fn argument_stub(argument: &Argument, modules_to_import: &mut BTreeSet) -> String { let mut output = argument.name.clone(); + if let Some(annotation) = &argument.annotation { + output.push_str(": "); + output.push_str(annotation); + if let Some((module, _)) = annotation.rsplit_once('.') { + // TODO: this is very naive + modules_to_import.insert(module.into()); + } + } if let Some(default_value) = &argument.default_value { - output.push('='); + output.push_str(if argument.annotation.is_some() { + " = " + } else { + "=" + }); output.push_str(default_value); } output @@ -99,10 +118,12 @@ mod tests { positional_only_arguments: vec![Argument { name: "posonly".into(), default_value: None, + annotation: None, }], arguments: vec![Argument { name: "arg".into(), default_value: None, + annotation: None, }], vararg: Some(VariableLengthArgument { name: "varargs".into(), @@ -110,6 +131,7 @@ mod tests { keyword_only_arguments: vec![Argument { name: "karg".into(), default_value: None, + annotation: Some("str".into()), }], kwarg: Some(VariableLengthArgument { name: "kwarg".into(), @@ -117,8 +139,8 @@ mod tests { }, }; assert_eq!( - "def func(posonly, /, arg, *varargs, karg, **kwarg): ...", - function_stubs(&function) + "def func(posonly, /, arg, *varargs, karg: str, **kwarg): ...", + function_stubs(&function, &mut BTreeSet::new()) ) } @@ -130,22 +152,25 @@ mod tests { positional_only_arguments: vec![Argument { name: "posonly".into(), default_value: Some("1".into()), + annotation: None, }], arguments: vec![Argument { name: "arg".into(), default_value: Some("True".into()), + annotation: None, }], vararg: None, keyword_only_arguments: vec![Argument { name: "karg".into(), default_value: Some("\"foo\"".into()), + annotation: Some("str".into()), }], kwarg: None, }, }; assert_eq!( - "def afunc(posonly=1, /, arg=True, *, karg=\"foo\"): ...", - function_stubs(&function) + "def afunc(posonly=1, /, arg=True, *, karg: str = \"foo\"): ...", + function_stubs(&function, &mut BTreeSet::new()) ) } } diff --git a/pyo3-introspection/tests/test.rs b/pyo3-introspection/tests/test.rs index 37070a53a13..598f9564d94 100644 --- a/pyo3-introspection/tests/test.rs +++ b/pyo3-introspection/tests/test.rs @@ -1,8 +1,11 @@ -use anyhow::Result; +use anyhow::{ensure, Result}; use pyo3_introspection::{introspect_cdylib, module_stub_files}; use std::collections::HashMap; +use std::io::{Read, Seek, SeekFrom, Write}; use std::path::{Path, PathBuf}; +use std::process::Command; use std::{env, fs}; +use tempfile::NamedTempFile; #[test] fn pytests_stubs() -> Result<()> { @@ -42,9 +45,12 @@ fn pytests_stubs() -> Result<()> { file_name.display() ) }); + + let actual_file_content = format_with_ruff(actual_file_content)?; + assert_eq!( - &expected_file_content.replace('\r', ""), // Windows compatibility - actual_file_content, + expected_file_content.as_str(), + actual_file_content.as_str(), "The content of file {} is different", file_name.display() ) @@ -75,3 +81,25 @@ fn add_dir_files( } Ok(()) } + +fn format_with_ruff(code: &str) -> Result { + let temp_file = NamedTempFile::with_suffix(".pyi")?; + // Write to file + { + let mut file = temp_file.as_file(); + file.write_all(code.as_bytes())?; + file.flush()?; + file.seek(SeekFrom::Start(0))?; + } + ensure!( + Command::new("ruff") + .arg("format") + .arg(temp_file.path()) + .status()? + .success(), + "Failed to run ruff" + ); + let mut content = String::new(); + temp_file.as_file().read_to_string(&mut content)?; + Ok(content) +} diff --git a/pyo3-macros-backend/src/introspection.rs b/pyo3-macros-backend/src/introspection.rs index 4888417cb08..abeb608c2b7 100644 --- a/pyo3-macros-backend/src/introspection.rs +++ b/pyo3-macros-backend/src/introspection.rs @@ -10,7 +10,7 @@ use crate::method::{FnArg, RegularArg}; use crate::pyfunction::FunctionSignature; -use crate::utils::PyO3CratePath; +use crate::utils::{PyO3CratePath, TypeExt}; use proc_macro2::{Span, TokenStream}; use quote::{format_ident, quote, ToTokens}; use std::borrow::Cow; @@ -19,7 +19,7 @@ use std::collections::HashMap; use std::hash::{Hash, Hasher}; use std::mem::take; use std::sync::atomic::{AtomicUsize, Ordering}; -use syn::{Attribute, Ident}; +use syn::{Attribute, Ident, Type}; static GLOBAL_COUNTER_FOR_UNIQUE_NAMES: AtomicUsize = AtomicUsize::new(0); @@ -179,12 +179,36 @@ fn argument_introspection_data<'a>( IntrospectionNode::String(desc.default_value().into()), ); } + 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 ty = ty.clone().elide_lifetimes(); + params.insert( + "annotation", + IntrospectionNode::InputType { + rust_type: ty, + nullable: true, + }, + ); + } else { + let ty = desc.ty.clone().elide_lifetimes(); + params.insert( + "annotation", + IntrospectionNode::InputType { + rust_type: ty, + nullable: false, + }, + ); + } + } IntrospectionNode::Map(params) } enum IntrospectionNode<'a> { String(Cow<'a, str>), IntrospectionId(Option<&'a Ident>), + InputType { rust_type: Type, nullable: bool }, Map(HashMap<&'static str, IntrospectionNode<'a>>), List(Vec>), } @@ -192,7 +216,7 @@ enum IntrospectionNode<'a> { impl IntrospectionNode<'_> { fn emit(self, pyo3_crate_path: &PyO3CratePath) -> TokenStream { let mut content = ConcatenationBuilder::default(); - self.add_to_serialization(&mut content); + self.add_to_serialization(&mut content, pyo3_crate_path); let content = content.into_token_stream(pyo3_crate_path); let static_name = format_ident!("PYO3_INTROSPECTION_0_{}", unique_element_id()); @@ -206,7 +230,11 @@ impl IntrospectionNode<'_> { } } - fn add_to_serialization(self, content: &mut ConcatenationBuilder) { + fn add_to_serialization( + self, + content: &mut ConcatenationBuilder, + pyo3_crate_path: &PyO3CratePath, + ) { match self { Self::String(string) => { content.push_str_to_escape(&string); @@ -216,10 +244,21 @@ impl IntrospectionNode<'_> { content.push_tokens(if let Some(ident) = ident { quote! { #ident::_PYO3_INTROSPECTION_ID } } else { - Ident::new("_PYO3_INTROSPECTION_ID", Span::call_site()).into_token_stream() + quote! { _PYO3_INTROSPECTION_ID } }); content.push_str("\""); } + Self::InputType { + rust_type, + nullable, + } => { + content.push_str("\""); + content.push_tokens(quote! { <#rust_type as #pyo3_crate_path::impl_::extract_argument::PyFunctionArgument>::INPUT_TYPE }); + if nullable { + content.push_str(" | None"); + } + content.push_str("\""); + } Self::Map(map) => { content.push_str("{"); for (i, (key, value)) in map.into_iter().enumerate() { @@ -228,7 +267,7 @@ impl IntrospectionNode<'_> { } content.push_str_to_escape(key); content.push_str(":"); - value.add_to_serialization(content); + value.add_to_serialization(content, pyo3_crate_path); } content.push_str("}"); } @@ -238,7 +277,7 @@ impl IntrospectionNode<'_> { if i > 0 { content.push_str(","); } - value.add_to_serialization(content); + value.add_to_serialization(content, pyo3_crate_path); } content.push_str("]"); } diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index ab86138338b..23f3060c923 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -2133,12 +2133,27 @@ impl<'a> PyClassImplsBuilder<'a> { fn impl_extractext(&self, ctx: &Ctx) -> TokenStream { let Ctx { pyo3_path, .. } = ctx; let cls = self.cls; + + let input_type = if cfg!(feature = "experimental-inspect") { + let cls_name = get_class_python_name(cls, self.attr).to_string(); + let full_name = if let Some(ModuleAttribute { value, .. }) = &self.attr.options.module { + let value = value.value(); + format!("{value}.{cls_name}") + } else { + cls_name + }; + quote! { const INPUT_TYPE: &'static str = #full_name; } + } else { + quote! {} + }; if self.attr.options.frozen.is_some() { quote! { impl<'a, 'py> #pyo3_path::impl_::extract_argument::PyFunctionArgument<'a, 'py, false> for &'a #cls { type Holder = ::std::option::Option<#pyo3_path::PyRef<'py, #cls>>; + #input_type + #[inline] fn extract(obj: &'a #pyo3_path::Bound<'py, #pyo3_path::PyAny>, holder: &'a mut Self::Holder) -> #pyo3_path::PyResult { #pyo3_path::impl_::extract_argument::extract_pyclass_ref(obj, holder) @@ -2151,6 +2166,8 @@ impl<'a> PyClassImplsBuilder<'a> { { type Holder = ::std::option::Option<#pyo3_path::PyRef<'py, #cls>>; + #input_type + #[inline] fn extract(obj: &'a #pyo3_path::Bound<'py, #pyo3_path::PyAny>, holder: &'a mut Self::Holder) -> #pyo3_path::PyResult { #pyo3_path::impl_::extract_argument::extract_pyclass_ref(obj, holder) @@ -2161,6 +2178,8 @@ impl<'a> PyClassImplsBuilder<'a> { { type Holder = ::std::option::Option<#pyo3_path::PyRefMut<'py, #cls>>; + #input_type + #[inline] fn extract(obj: &'a #pyo3_path::Bound<'py, #pyo3_path::PyAny>, holder: &'a mut Self::Holder) -> #pyo3_path::PyResult { #pyo3_path::impl_::extract_argument::extract_pyclass_ref_mut(obj, holder) diff --git a/pytests/stubs/pyfunctions.pyi b/pytests/stubs/pyfunctions.pyi index 5fb5e6c474c..b74b2f1fc61 100644 --- a/pytests/stubs/pyfunctions.pyi +++ b/pytests/stubs/pyfunctions.pyi @@ -1,8 +1,22 @@ +import typing + def args_kwargs(*args, **kwargs): ... def none(): ... -def positional_only(a, /, b): ... -def simple(a, b=None, *, c=None): ... -def simple_args(a, b=None, *args, c=None): ... -def simple_args_kwargs(a, b=None, *args, c=None, **kwargs): ... -def simple_kwargs(a, b=None, c=None, **kwargs): ... -def with_typed_args(a=False, b=0, c=0.0, d=""): ... +def positional_only(a: typing.Any, /, b: typing.Any): ... +def simple( + a: typing.Any, b: typing.Any | None = None, *, c: typing.Any | None = None +): ... +def simple_args( + a: typing.Any, b: typing.Any | None = None, *args, c: typing.Any | None = None +): ... +def simple_args_kwargs( + a: typing.Any, + b: typing.Any | None = None, + *args, + c: typing.Any | None = None, + **kwargs, +): ... +def simple_kwargs( + a: typing.Any, b: typing.Any | None = None, c: typing.Any | None = None, **kwargs +): ... +def with_typed_args(a: bool = False, b: int = 0, c: float = 0.0, d: str = ""): ... diff --git a/src/conversion.rs b/src/conversion.rs index 165175fae54..5fba9b8dc30 100644 --- a/src/conversion.rs +++ b/src/conversion.rs @@ -277,6 +277,13 @@ impl<'py, T> IntoPyObjectExt<'py> for T where T: IntoPyObject<'py> {} /// infinite recursion, implementors must implement at least one of these methods. The recommendation /// is to implement `extract_bound` and leave `extract` as the default implementation. pub trait FromPyObject<'py>: Sized { + /// Provides the type hint information for this type when it appears as an argument. + /// + /// For example, `Vec` would be `collections.abc.Sequence[int]`. + /// The default value is `typing.Any`, which is correct for any type. + #[cfg(feature = "experimental-inspect")] + const INPUT_TYPE: &'static str = "typing.Any"; + /// Extracts `Self` from the bound smart pointer `obj`. /// /// Implementors are encouraged to implement this method and leave `extract` defaulted, as @@ -339,6 +346,13 @@ mod from_py_object_bound_sealed { /// Similarly, users should typically not call these trait methods and should instead /// use this via the `extract` method on `Bound` and `Py`. pub trait FromPyObjectBound<'a, 'py>: Sized + from_py_object_bound_sealed::Sealed { + /// Provides the type hint information for this type when it appears as an argument. + /// + /// For example, `Vec` would be `collections.abc.Sequence[int]`. + /// The default value is `typing.Any`, which is correct for any type. + #[cfg(feature = "experimental-inspect")] + const INPUT_TYPE: &'static str = "typing.Any"; + /// Extracts `Self` from the bound smart pointer `obj`. /// /// Users are advised against calling this method directly: instead, use this via @@ -363,6 +377,9 @@ impl<'py, T> FromPyObjectBound<'_, 'py> for T where T: FromPyObject<'py>, { + #[cfg(feature = "experimental-inspect")] + const INPUT_TYPE: &'static str = T::INPUT_TYPE; + fn from_py_object_bound(ob: Borrowed<'_, 'py, PyAny>) -> PyResult { Self::extract_bound(&ob) } diff --git a/src/conversions/std/num.rs b/src/conversions/std/num.rs index ea5798cac99..b7a8d0b472b 100644 --- a/src/conversions/std/num.rs +++ b/src/conversions/std/num.rs @@ -46,6 +46,9 @@ macro_rules! int_fits_larger_int { } impl FromPyObject<'_> for $rust_type { + #[cfg(feature = "experimental-inspect")] + const INPUT_TYPE: &'static str = <$larger_type>::INPUT_TYPE; + fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult { let val: $larger_type = obj.extract()?; <$rust_type>::try_from(val) @@ -121,6 +124,9 @@ macro_rules! int_convert_u64_or_i64 { } } impl FromPyObject<'_> for $rust_type { + #[cfg(feature = "experimental-inspect")] + const INPUT_TYPE: &'static str = "int"; + fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult<$rust_type> { extract_int!(obj, !0, $pylong_as_ll_or_ull, $force_index_call) } @@ -171,6 +177,9 @@ macro_rules! int_fits_c_long { } impl<'py> FromPyObject<'py> for $rust_type { + #[cfg(feature = "experimental-inspect")] + const INPUT_TYPE: &'static str = "int"; + fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult { let val: c_long = extract_int!(obj, -1, ffi::PyLong_AsLong)?; <$rust_type>::try_from(val) @@ -245,6 +254,9 @@ impl<'py> IntoPyObject<'py> for &'_ u8 { } impl FromPyObject<'_> for u8 { + #[cfg(feature = "experimental-inspect")] + const INPUT_TYPE: &'static str = "int"; + fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult { let val: c_long = extract_int!(obj, -1, ffi::PyLong_AsLong)?; u8::try_from(val).map_err(|e| exceptions::PyOverflowError::new_err(e.to_string())) @@ -367,6 +379,9 @@ mod fast_128bit_int_conversion { } impl FromPyObject<'_> for $rust_type { + #[cfg(feature = "experimental-inspect")] + const INPUT_TYPE: &'static str = "int"; + fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult<$rust_type> { let num = unsafe { ffi::PyNumber_Index(ob.as_ptr()).assume_owned_or_err(ob.py())? }; @@ -476,6 +491,9 @@ mod slow_128bit_int_conversion { } impl FromPyObject<'_> for $rust_type { + #[cfg(feature = "experimental-inspect")] + const INPUT_TYPE: &'static str = "int"; + fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult<$rust_type> { let py = ob.py(); unsafe { @@ -555,6 +573,9 @@ macro_rules! nonzero_int_impl { } impl FromPyObject<'_> for $nonzero_type { + #[cfg(feature = "experimental-inspect")] + const INPUT_TYPE: &'static str = <$primitive_type>::INPUT_TYPE; + fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult { let val: $primitive_type = obj.extract()?; <$nonzero_type>::try_from(val) diff --git a/src/conversions/std/string.rs b/src/conversions/std/string.rs index 8936bd35000..48e47e5e46d 100644 --- a/src/conversions/std/string.rs +++ b/src/conversions/std/string.rs @@ -138,6 +138,9 @@ impl<'py> IntoPyObject<'py> for &String { #[cfg(any(Py_3_10, not(Py_LIMITED_API)))] impl<'a> crate::conversion::FromPyObjectBound<'a, '_> for &'a str { + #[cfg(feature = "experimental-inspect")] + const INPUT_TYPE: &'static str = "str"; + fn from_py_object_bound(ob: crate::Borrowed<'a, '_, PyAny>) -> PyResult { ob.downcast::()?.to_str() } @@ -149,6 +152,9 @@ impl<'a> crate::conversion::FromPyObjectBound<'a, '_> for &'a str { } impl<'a> crate::conversion::FromPyObjectBound<'a, '_> for Cow<'a, str> { + #[cfg(feature = "experimental-inspect")] + const INPUT_TYPE: &'static str = "str"; + fn from_py_object_bound(ob: crate::Borrowed<'a, '_, PyAny>) -> PyResult { ob.downcast::()?.to_cow() } @@ -162,6 +168,9 @@ impl<'a> crate::conversion::FromPyObjectBound<'a, '_> for Cow<'a, str> { /// Allows extracting strings from Python objects. /// Accepts Python `str` and `unicode` objects. impl FromPyObject<'_> for String { + #[cfg(feature = "experimental-inspect")] + const INPUT_TYPE: &'static str = "str"; + fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult { obj.downcast::()?.to_cow().map(Cow::into_owned) } @@ -173,6 +182,9 @@ impl FromPyObject<'_> for String { } impl FromPyObject<'_> for char { + #[cfg(feature = "experimental-inspect")] + const INPUT_TYPE: &'static str = "str"; + fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult { let s = obj.downcast::()?.to_cow()?; let mut iter = s.chars(); diff --git a/src/impl_/extract_argument.rs b/src/impl_/extract_argument.rs index 7107c51b26a..5dc67677cfb 100644 --- a/src/impl_/extract_argument.rs +++ b/src/impl_/extract_argument.rs @@ -22,6 +22,11 @@ type PyArg<'py> = Borrowed<'py, 'py, PyAny>; /// There exists a trivial blanket implementation for `T: FromPyObject` with `Holder = ()`. pub trait PyFunctionArgument<'a, 'py, const IS_OPTION: bool>: Sized + 'a { type Holder: FunctionArgumentHolder; + + /// Provides the type hint information for which Python types are allowed. + #[cfg(feature = "experimental-inspect")] + const INPUT_TYPE: &'static str; + fn extract(obj: &'a Bound<'py, PyAny>, holder: &'a mut Self::Holder) -> PyResult; } @@ -31,6 +36,9 @@ where { type Holder = (); + #[cfg(feature = "experimental-inspect")] + const INPUT_TYPE: &'static str = T::INPUT_TYPE; + #[inline] fn extract(obj: &'a Bound<'py, PyAny>, _: &'a mut ()) -> PyResult { obj.extract() @@ -43,6 +51,9 @@ where { type Holder = (); + #[cfg(feature = "experimental-inspect")] + const INPUT_TYPE: &'static str = T::PYTHON_TYPE; + #[inline] fn extract(obj: &'a Bound<'py, PyAny>, _: &'a mut ()) -> PyResult { obj.downcast().map_err(Into::into) @@ -55,6 +66,9 @@ where { type Holder = T::Holder; + #[cfg(feature = "experimental-inspect")] + const INPUT_TYPE: &'static str = "typing.Any | None"; + #[inline] fn extract(obj: &'a Bound<'py, PyAny>, holder: &'a mut T::Holder) -> PyResult { if obj.is_none() { @@ -69,6 +83,9 @@ where impl<'a> PyFunctionArgument<'a, '_, false> for &'a str { type Holder = Option>; + #[cfg(feature = "experimental-inspect")] + const INPUT_TYPE: &'static str = "str"; + #[inline] fn extract( obj: &'a Bound<'_, PyAny>, diff --git a/src/type_object.rs b/src/type_object.rs index 778fea7152c..2d7c0f9ef2c 100644 --- a/src/type_object.rs +++ b/src/type_object.rs @@ -43,6 +43,10 @@ pub unsafe trait PyTypeInfo: Sized { /// Module name, if any. const MODULE: Option<&'static str>; + /// Provides the full python type paths. + #[cfg(feature = "experimental-inspect")] + const PYTHON_TYPE: &'static str = "typing.Any"; + /// Returns the PyTypeObject instance for this type. fn type_object_raw(py: Python<'_>) -> *mut ffi::PyTypeObject; @@ -86,6 +90,10 @@ pub trait PyTypeCheck { /// Name of self. This is used in error messages, for example. const NAME: &'static str; + /// Provides the full python type of the allowed values. + #[cfg(feature = "experimental-inspect")] + const PYTHON_TYPE: &'static str; + /// Checks if `object` is an instance of `Self`, which may include a subtype. /// /// This should be equivalent to the Python expression `isinstance(object, Self)`. @@ -98,6 +106,9 @@ where { const NAME: &'static str = ::NAME; + #[cfg(feature = "experimental-inspect")] + const PYTHON_TYPE: &'static str = ::PYTHON_TYPE; + #[inline] fn type_check(object: &Bound<'_, PyAny>) -> bool { T::is_type_of(object) diff --git a/src/types/boolobject.rs b/src/types/boolobject.rs index 28806f8c4cf..6fc2e87b2d8 100644 --- a/src/types/boolobject.rs +++ b/src/types/boolobject.rs @@ -172,6 +172,9 @@ impl<'py> IntoPyObject<'py> for &bool { /// /// Fails with `TypeError` if the input is not a Python `bool`. impl FromPyObject<'_> for bool { + #[cfg(feature = "experimental-inspect")] + const INPUT_TYPE: &'static str = "bool"; + fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult { let err = match obj.downcast::() { Ok(obj) => return Ok(obj.is_true()), diff --git a/src/types/datetime.rs b/src/types/datetime.rs index 8690952807d..8a9c4166aa6 100644 --- a/src/types/datetime.rs +++ b/src/types/datetime.rs @@ -239,6 +239,8 @@ pyobject_native_type_named!(PyDate); #[cfg(Py_LIMITED_API)] impl PyTypeCheck for PyDate { const NAME: &'static str = "PyDate"; + #[cfg(feature = "experimental-inspect")] + const PYTHON_TYPE: &'static str = "datetime.date"; fn type_check(object: &Bound<'_, PyAny>) -> bool { let py = object.py(); @@ -338,6 +340,8 @@ pyobject_native_type_named!(PyDateTime); #[cfg(Py_LIMITED_API)] impl PyTypeCheck for PyDateTime { const NAME: &'static str = "PyDateTime"; + #[cfg(feature = "experimental-inspect")] + const PYTHON_TYPE: &'static str = "datetime.datetime"; fn type_check(object: &Bound<'_, PyAny>) -> bool { let py = object.py(); @@ -587,6 +591,8 @@ pyobject_native_type_named!(PyTime); #[cfg(Py_LIMITED_API)] impl PyTypeCheck for PyTime { const NAME: &'static str = "PyTime"; + #[cfg(feature = "experimental-inspect")] + const PYTHON_TYPE: &'static str = "datetime.time"; fn type_check(object: &Bound<'_, PyAny>) -> bool { let py = object.py(); @@ -771,6 +777,8 @@ pyobject_native_type_named!(PyTzInfo); #[cfg(Py_LIMITED_API)] impl PyTypeCheck for PyTzInfo { const NAME: &'static str = "PyTzInfo"; + #[cfg(feature = "experimental-inspect")] + const PYTHON_TYPE: &'static str = "datetime.tzinfo"; fn type_check(object: &Bound<'_, PyAny>) -> bool { let py = object.py(); @@ -885,6 +893,8 @@ pyobject_native_type_named!(PyDelta); #[cfg(Py_LIMITED_API)] impl PyTypeCheck for PyDelta { const NAME: &'static str = "PyDelta"; + #[cfg(feature = "experimental-inspect")] + const PYTHON_TYPE: &'static str = "datetime.timedelta"; fn type_check(object: &Bound<'_, PyAny>) -> bool { let py = object.py(); diff --git a/src/types/float.rs b/src/types/float.rs index 3753572904b..2cc95246996 100644 --- a/src/types/float.rs +++ b/src/types/float.rs @@ -102,8 +102,11 @@ impl<'py> IntoPyObject<'py> for &f64 { } impl<'py> FromPyObject<'py> for f64 { + #[cfg(feature = "experimental-inspect")] + const INPUT_TYPE: &'static str = "float"; + // PyFloat_AsDouble returns -1.0 upon failure - #![allow(clippy::float_cmp)] + #[allow(clippy::float_cmp)] fn extract_bound(obj: &Bound<'py, PyAny>) -> PyResult { // On non-limited API, .value() uses PyFloat_AS_DOUBLE which // allows us to have an optimized fast path for the case when @@ -164,6 +167,9 @@ impl<'py> IntoPyObject<'py> for &f32 { } impl<'py> FromPyObject<'py> for f32 { + #[cfg(feature = "experimental-inspect")] + const INPUT_TYPE: &'static str = "float"; + fn extract_bound(obj: &Bound<'py, PyAny>) -> PyResult { Ok(obj.extract::()? as f32) } diff --git a/src/types/iterator.rs b/src/types/iterator.rs index 6731fff2c48..6b7cf722337 100644 --- a/src/types/iterator.rs +++ b/src/types/iterator.rs @@ -119,6 +119,8 @@ impl<'py> IntoIterator for &Bound<'py, PyIterator> { impl PyTypeCheck for PyIterator { const NAME: &'static str = "Iterator"; + #[cfg(feature = "experimental-inspect")] + const PYTHON_TYPE: &'static str = "collections.abc.Iterator"; fn type_check(object: &Bound<'_, PyAny>) -> bool { unsafe { ffi::PyIter_Check(object.as_ptr()) != 0 } diff --git a/src/types/mapping.rs b/src/types/mapping.rs index baf3a023ce9..5950db72743 100644 --- a/src/types/mapping.rs +++ b/src/types/mapping.rs @@ -168,6 +168,8 @@ fn get_mapping_abc(py: Python<'_>) -> PyResult<&Bound<'_, PyType>> { impl PyTypeCheck for PyMapping { const NAME: &'static str = "Mapping"; + #[cfg(feature = "experimental-inspect")] + const PYTHON_TYPE: &'static str = "collections.abc.Mapping"; #[inline] fn type_check(object: &Bound<'_, PyAny>) -> bool { diff --git a/src/types/sequence.rs b/src/types/sequence.rs index c5fef02bd5e..2546b483baf 100644 --- a/src/types/sequence.rs +++ b/src/types/sequence.rs @@ -377,6 +377,8 @@ fn get_sequence_abc(py: Python<'_>) -> PyResult<&Bound<'_, PyType>> { impl PyTypeCheck for PySequence { const NAME: &'static str = "Sequence"; + #[cfg(feature = "experimental-inspect")] + const PYTHON_TYPE: &'static str = "collections.abc.Sequence"; #[inline] fn type_check(object: &Bound<'_, PyAny>) -> bool { diff --git a/src/types/weakref/anyref.rs b/src/types/weakref/anyref.rs index d496c175376..9a481d2b0c0 100644 --- a/src/types/weakref/anyref.rs +++ b/src/types/weakref/anyref.rs @@ -18,6 +18,8 @@ pyobject_native_type_named!(PyWeakref); impl PyTypeCheck for PyWeakref { const NAME: &'static str = "weakref"; + #[cfg(feature = "experimental-inspect")] + const PYTHON_TYPE: &'static str = "weakref.ProxyTypes"; fn type_check(object: &Bound<'_, PyAny>) -> bool { unsafe { ffi::PyWeakref_Check(object.as_ptr()) > 0 } diff --git a/src/types/weakref/proxy.rs b/src/types/weakref/proxy.rs index f60321fdd3d..ceab81e37a5 100644 --- a/src/types/weakref/proxy.rs +++ b/src/types/weakref/proxy.rs @@ -22,6 +22,8 @@ pyobject_native_type_named!(PyWeakrefProxy); impl PyTypeCheck for PyWeakrefProxy { const NAME: &'static str = "weakref.ProxyTypes"; + #[cfg(feature = "experimental-inspect")] + const PYTHON_TYPE: &'static str = "weakref.ProxyType | weakref.CallableProxyType"; fn type_check(object: &Bound<'_, PyAny>) -> bool { unsafe { ffi::PyWeakref_CheckProxy(object.as_ptr()) > 0 } diff --git a/src/types/weakref/reference.rs b/src/types/weakref/reference.rs index a4540f7eaf9..eb4eb53a170 100644 --- a/src/types/weakref/reference.rs +++ b/src/types/weakref/reference.rs @@ -35,6 +35,8 @@ pyobject_native_type_named!(PyWeakrefReference); #[cfg(any(PyPy, GraalPy, Py_LIMITED_API))] impl PyTypeCheck for PyWeakrefReference { const NAME: &'static str = "weakref.ReferenceType"; + #[cfg(feature = "experimental-inspect")] + const PYTHON_TYPE: &'static str = "weakref.ReferenceType"; fn type_check(object: &Bound<'_, PyAny>) -> bool { unsafe { ffi::PyWeakref_CheckRef(object.as_ptr()) > 0 } diff --git a/tests/ui/invalid_cancel_handle.stderr b/tests/ui/invalid_cancel_handle.stderr index a023d128bf2..ca9274f1785 100644 --- a/tests/ui/invalid_cancel_handle.stderr +++ b/tests/ui/invalid_cancel_handle.stderr @@ -22,6 +22,32 @@ error: `from_py_with` and `cancel_handle` cannot be specified together 24 | #[pyo3(cancel_handle, from_py_with = cancel_handle)] _param: pyo3::coroutine::CancelHandle, | ^^^^^^^^^^^^^ +error[E0277]: the trait bound `CancelHandle: PyFunctionArgument<'_, '_, false>` is not satisfied + --> tests/ui/invalid_cancel_handle.rs:20:50 + | +20 | async fn missing_cancel_handle_attribute(_param: pyo3::coroutine::CancelHandle) {} + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `PyClass` is not implemented for `CancelHandle` + | + = help: the trait `PyClass` is implemented for `pyo3::coroutine::Coroutine` + = note: required for `CancelHandle` to implement `FromPyObject<'_>` + = note: required for `CancelHandle` to implement `FromPyObjectBound<'_, '_>` + = note: required for `CancelHandle` to implement `PyFunctionArgument<'_, '_, false>` + +error[E0277]: the trait bound `CancelHandle: PyFunctionArgument<'_, '_, false>` is not satisfied + --> tests/ui/invalid_cancel_handle.rs:20:50 + | +20 | async fn missing_cancel_handle_attribute(_param: pyo3::coroutine::CancelHandle) {} + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Clone` is not implemented for `CancelHandle` + | + = help: the following other types implement trait `PyFunctionArgument<'a, 'py, IS_OPTION>`: + `&'a mut pyo3::coroutine::Coroutine` implements `PyFunctionArgument<'a, 'py, false>` + `&'a pyo3::Bound<'py, T>` implements `PyFunctionArgument<'a, 'py, false>` + `&'a pyo3::coroutine::Coroutine` implements `PyFunctionArgument<'a, 'py, false>` + `Option` implements `PyFunctionArgument<'a, 'py, true>` + = note: required for `CancelHandle` to implement `FromPyObject<'_>` + = note: required for `CancelHandle` to implement `FromPyObjectBound<'_, '_>` + = note: required for `CancelHandle` to implement `PyFunctionArgument<'_, '_, false>` + error[E0308]: mismatched types --> tests/ui/invalid_cancel_handle.rs:16:1 |