From b91c40c6f8bed4f1f665c7f17d6f80a4835e7ee3 Mon Sep 17 00:00:00 2001 From: Thomas Pellissier-Tanon Date: Wed, 10 Sep 2025 16:38:10 +0200 Subject: [PATCH 1/2] Introduce TypeHint struct inspect::TypeHint is composed of an "annotation" string and a list of "imports" ("from X import Y" kind) The type is expected to be built using the macros `type_hint!(module, name)`, `type_hint_union!(*args)` and `type_hint_subscript(main, *args)` that take care of maintaining the import list Introspection data generation is done using the hidden type_hint_json macro to avoid that the proc macros generate too much code Sadly, outside `type_hint` these macros can't be converted into const functions because they need to do some concatenation. I introduced `type_hint!` for consistency, happy to convert it to a const function. Miscellaneous changes: - Rename PyType{Info,Check}::TYPE_INFO into TYPE_HINT - Drop redundant PyClassImpl::TYPE_NAME --- newsfragments/5438.changed.md | 1 + pyo3-introspection/Cargo.toml | 1 - pyo3-introspection/src/introspection.rs | 98 +++++++-- pyo3-introspection/src/model.rs | 22 +- pyo3-introspection/src/stubs.rs | 246 +++++++++++------------ pyo3-macros-backend/src/frompyobject.rs | 71 +++---- pyo3-macros-backend/src/intopyobject.rs | 81 ++++---- pyo3-macros-backend/src/introspection.rs | 58 ++++-- pyo3-macros-backend/src/pyclass.rs | 56 +++--- pytests/stubs/__init__.pyi | 4 +- pytests/stubs/consts.pyi | 9 +- pytests/stubs/pyclasses.pyi | 10 +- pytests/stubs/pyfunctions.pyi | 60 +++--- src/conversion.rs | 16 +- src/conversions/std/cell.rs | 8 +- src/conversions/std/num.rs | 46 +++-- src/conversions/std/string.rs | 31 +-- src/impl_/extract_argument.rs | 18 +- src/impl_/introspection.rs | 42 +++- src/impl_/pyclass.rs | 3 - src/inspect/mod.rs | 213 ++++++++++++++++++++ src/instance.rs | 6 +- src/pycell.rs | 10 +- src/pyclass/guard.rs | 4 +- src/type_object.rs | 14 +- src/types/boolobject.rs | 17 +- src/types/float.rs | 16 +- src/types/weakref/anyref.rs | 10 +- src/types/weakref/proxy.rs | 12 +- 29 files changed, 776 insertions(+), 407 deletions(-) create mode 100644 newsfragments/5438.changed.md diff --git a/newsfragments/5438.changed.md b/newsfragments/5438.changed.md new file mode 100644 index 00000000000..dd713ae30c1 --- /dev/null +++ b/newsfragments/5438.changed.md @@ -0,0 +1 @@ +Introspection: introduce `TypeHint` and make use of it to encode type hint annotations. \ No newline at end of file diff --git a/pyo3-introspection/Cargo.toml b/pyo3-introspection/Cargo.toml index 0aa76535cc6..b58b28c09fb 100644 --- a/pyo3-introspection/Cargo.toml +++ b/pyo3-introspection/Cargo.toml @@ -13,7 +13,6 @@ anyhow = "1" goblin = ">=0.9, <0.11" serde = { version = "1", features = ["derive"] } serde_json = "1" -unicode-ident = "1" [dev-dependencies] tempfile = "3.12.0" diff --git a/pyo3-introspection/src/introspection.rs b/pyo3-introspection/src/introspection.rs index 2a5b94931f9..b91d0ceb9d2 100644 --- a/pyo3-introspection/src/introspection.rs +++ b/pyo3-introspection/src/introspection.rs @@ -1,5 +1,6 @@ use crate::model::{ - Argument, Arguments, Attribute, Class, Function, Module, VariableLengthArgument, + Argument, Arguments, Attribute, Class, Function, Module, TypeHint, TypeHintImport, + VariableLengthArgument, }; use anyhow::{anyhow, bail, ensure, Context, Result}; use goblin::elf::section_header::SHN_XINDEX; @@ -9,11 +10,12 @@ use goblin::mach::symbols::{NO_SECT, N_SECT}; use goblin::mach::{Mach, MachO, SingleArch}; use goblin::pe::PE; use goblin::Object; -use serde::Deserialize; +use serde::de::{Error, MapAccess, Visitor}; +use serde::{de, Deserialize, Deserializer}; use std::cmp::Ordering; use std::collections::HashMap; use std::path::Path; -use std::{fs, str}; +use std::{fs, str, fmt}; /// Introspect a cdylib built with PyO3 and returns the definition of a Python module. /// @@ -192,7 +194,7 @@ fn convert_function( name: &str, arguments: &ChunkArguments, decorators: &[String], - returns: &Option, + returns: &Option, ) -> Function { Function { name: name.into(), @@ -210,7 +212,7 @@ fn convert_function( .as_ref() .map(convert_variable_length_argument), }, - returns: returns.clone(), + returns: returns.as_ref().map(convert_type_hint), } } @@ -218,22 +220,40 @@ fn convert_argument(arg: &ChunkArgument) -> Argument { Argument { name: arg.name.clone(), default_value: arg.default.clone(), - annotation: arg.annotation.clone(), + annotation: arg.annotation.as_ref().map(convert_type_hint), } } fn convert_variable_length_argument(arg: &ChunkArgument) -> VariableLengthArgument { VariableLengthArgument { name: arg.name.clone(), - annotation: arg.annotation.clone(), + annotation: arg.annotation.as_ref().map(convert_type_hint), } } -fn convert_attribute(name: &str, value: &Option, annotation: &Option) -> Attribute { +fn convert_attribute( + name: &str, + value: &Option, + annotation: &Option, +) -> Attribute { Attribute { name: name.into(), value: value.clone(), - annotation: annotation.clone(), + annotation: annotation.as_ref().map(convert_type_hint), + } +} + +fn convert_type_hint(arg: &ChunkTypeHint) -> TypeHint { + TypeHint { + annotation: arg.annotation.clone(), + imports: arg.imports.iter().map(convert_type_hint_import).collect(), + } +} + +fn convert_type_hint_import(arg: &ChunkTypeHintImport) -> TypeHintImport { + TypeHintImport { + module: arg.module.clone(), + name: arg.name.clone(), } } @@ -388,8 +408,8 @@ enum Chunk { parent: Option, #[serde(default)] decorators: Vec, - #[serde(default)] - returns: Option, + #[serde(default, deserialize_with = "deserialize_annotation")] + returns: Option, }, Attribute { #[serde(default)] @@ -399,8 +419,8 @@ enum Chunk { name: String, #[serde(default)] value: Option, - #[serde(default)] - annotation: Option, + #[serde(default, deserialize_with = "deserialize_annotation")] + annotation: Option, }, } @@ -423,6 +443,56 @@ struct ChunkArgument { name: String, #[serde(default)] default: Option, + #[serde(default, deserialize_with = "deserialize_annotation")] + annotation: Option, +} + +#[derive(Deserialize)] +struct ChunkTypeHint { + annotation: String, #[serde(default)] - annotation: Option, + imports: Vec, +} + +#[derive(Deserialize)] +struct ChunkTypeHintImport { + module: String, + name: String, +} + +fn deserialize_annotation<'de, D: Deserializer<'de>>( + deserializer: D, +) -> Result, D::Error> { + struct AnnotationVisitor; + + impl<'de> Visitor<'de> for AnnotationVisitor { + type Value = ChunkTypeHint; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("annotation") + } + + fn visit_str(self, v: &str) -> Result + where + E: Error, + { + self.visit_string(v.into()) + } + + fn visit_string(self, v: String) -> Result + where + E: Error, + { + Ok(ChunkTypeHint { + annotation: v, + imports: Vec::new(), + }) + } + + fn visit_map>(self, map: M) -> Result { + Deserialize::deserialize(de::value::MapAccessDeserializer::new(map)) + } + } + + Ok(Some(deserializer.deserialize_any(AnnotationVisitor)?)) } diff --git a/pyo3-introspection/src/model.rs b/pyo3-introspection/src/model.rs index 9f86bb7e303..dfcb42cea7d 100644 --- a/pyo3-introspection/src/model.rs +++ b/pyo3-introspection/src/model.rs @@ -22,7 +22,7 @@ pub struct Function { pub decorators: Vec, pub arguments: Arguments, /// return type - pub returns: Option, + pub returns: Option, } #[derive(Debug, Eq, PartialEq, Clone, Hash)] @@ -31,7 +31,7 @@ pub struct Attribute { /// Value as a Python expression if easily expressible pub value: Option, /// Type annotation as a Python expression - pub annotation: Option, + pub annotation: Option, } #[derive(Debug, Eq, PartialEq, Clone, Hash)] @@ -54,7 +54,7 @@ pub struct Argument { /// Default value as a Python expression pub default_value: Option, /// Type annotation as a Python expression - pub annotation: Option, + pub annotation: Option, } /// A variable length argument ie. *vararg or **kwarg @@ -62,5 +62,19 @@ pub struct Argument { pub struct VariableLengthArgument { pub name: String, /// Type annotation as a Python expression - pub annotation: Option, + pub annotation: Option, +} + +/// A type hint annotation with the required modules to import +#[derive(Debug, Eq, PartialEq, Clone, Hash)] +pub struct TypeHint { + pub annotation: String, + pub imports: Vec, +} + +/// An import required to make the type hint valid like `from {module} import {name}` +#[derive(Debug, Eq, PartialEq, Clone, Hash)] +pub struct TypeHintImport { + pub module: String, + pub name: String, } diff --git a/pyo3-introspection/src/stubs.rs b/pyo3-introspection/src/stubs.rs index baad91dd6e2..e41470e8a39 100644 --- a/pyo3-introspection/src/stubs.rs +++ b/pyo3-introspection/src/stubs.rs @@ -1,9 +1,9 @@ use crate::model::{ - Argument, Arguments, Attribute, Class, Function, Module, VariableLengthArgument, + Argument, Arguments, Attribute, Class, Function, Module, TypeHint, TypeHintImport, + VariableLengthArgument, }; -use std::collections::{BTreeSet, HashMap}; -use std::path::{Path, PathBuf}; -use unicode_ident::{is_xid_continue, is_xid_start}; +use std::collections::{BTreeMap, BTreeSet, HashMap}; +use std::path::PathBuf; /// Generates the [type stubs](https://typing.readthedocs.io/en/latest/source/stubs.html) of a given module. /// It returns a map between the file name and the file content. @@ -11,40 +11,49 @@ use unicode_ident::{is_xid_continue, is_xid_start}; /// in files with a relevant name. pub fn module_stub_files(module: &Module) -> HashMap { let mut output_files = HashMap::new(); - add_module_stub_files(module, Path::new(""), &mut output_files); + add_module_stub_files(module, &[], &mut output_files); output_files } fn add_module_stub_files( module: &Module, - module_path: &Path, + module_path: &[&str], output_files: &mut HashMap, ) { - output_files.insert(module_path.join("__init__.pyi"), module_stubs(module)); + let mut file_path = PathBuf::new(); + for e in module_path { + file_path = file_path.join(e); + } + output_files.insert( + file_path.join("__init__.pyi"), + module_stubs(module, module_path), + ); + let mut module_path = module_path.to_vec(); + module_path.push(&module.name); for submodule in &module.modules { if submodule.modules.is_empty() { output_files.insert( - module_path.join(format!("{}.pyi", submodule.name)), - module_stubs(submodule), + file_path.join(format!("{}.pyi", submodule.name)), + module_stubs(submodule, &module_path), ); } else { - add_module_stub_files(submodule, &module_path.join(&submodule.name), output_files); + add_module_stub_files(submodule, &module_path, output_files); } } } /// Generates the module stubs to a String, not including submodules -fn module_stubs(module: &Module) -> String { - let mut modules_to_import = BTreeSet::new(); +fn module_stubs(module: &Module, parents: &[&str]) -> String { + let mut imports = Imports::new(); let mut elements = Vec::new(); for attribute in &module.attributes { - elements.push(attribute_stubs(attribute, &mut modules_to_import)); + elements.push(attribute_stubs(attribute, &mut imports)); } for class in &module.classes { - elements.push(class_stubs(class, &mut modules_to_import)); + elements.push(class_stubs(class, &mut imports)); } for function in &module.functions { - elements.push(function_stubs(function, &mut modules_to_import)); + elements.push(function_stubs(function, &mut imports)); } // We generate a __getattr__ method to tag incomplete stubs @@ -59,22 +68,31 @@ fn module_stubs(module: &Module) -> String { arguments: vec![Argument { name: "name".to_string(), default_value: None, - annotation: Some("str".into()), + annotation: Some(TypeHint { + annotation: "str".into(), + imports: Vec::new(), + }), }], vararg: None, keyword_only_arguments: Vec::new(), kwarg: None, }, - returns: Some("_typeshed.Incomplete".into()), + returns: Some(TypeHint { + annotation: "Incomplete".into(), + imports: vec![TypeHintImport { + module: "_typeshed".into(), + name: "Incomplete".into(), + }], + }), }, - &mut modules_to_import, + &mut imports, )); } - let mut final_elements = Vec::new(); - for module_to_import in &modules_to_import { - final_elements.push(format!("import {module_to_import}")); - } + // We validate the imports + imports.filter_for_module(&module.name, parents); + + let mut final_elements = imports.to_lines(); final_elements.extend(elements); let mut output = String::new(); @@ -99,7 +117,7 @@ fn module_stubs(module: &Module) -> String { output } -fn class_stubs(class: &Class, modules_to_import: &mut BTreeSet) -> String { +fn class_stubs(class: &Class, imports: &mut Imports) -> String { let mut buffer = format!("class {}:", class.name); if class.methods.is_empty() && class.attributes.is_empty() { buffer.push_str(" ..."); @@ -108,43 +126,43 @@ fn class_stubs(class: &Class, modules_to_import: &mut BTreeSet) -> Strin for attribute in &class.attributes { // We do the indentation buffer.push_str("\n "); - buffer.push_str(&attribute_stubs(attribute, modules_to_import).replace('\n', "\n ")); + buffer.push_str(&attribute_stubs(attribute, imports).replace('\n', "\n ")); } for method in &class.methods { // We do the indentation buffer.push_str("\n "); - buffer.push_str(&function_stubs(method, modules_to_import).replace('\n', "\n ")); + buffer.push_str(&function_stubs(method, imports).replace('\n', "\n ")); } buffer } -fn function_stubs(function: &Function, modules_to_import: &mut BTreeSet) -> String { +fn function_stubs(function: &Function, imports: &mut Imports) -> String { // Signature let mut parameters = Vec::new(); for argument in &function.arguments.positional_only_arguments { - parameters.push(argument_stub(argument, modules_to_import)); + parameters.push(argument_stub(argument, imports)); } if !function.arguments.positional_only_arguments.is_empty() { parameters.push("/".into()); } for argument in &function.arguments.arguments { - parameters.push(argument_stub(argument, modules_to_import)); + parameters.push(argument_stub(argument, imports)); } if let Some(argument) = &function.arguments.vararg { parameters.push(format!( "*{}", - variable_length_argument_stub(argument, modules_to_import) + variable_length_argument_stub(argument, imports) )); } else if !function.arguments.keyword_only_arguments.is_empty() { parameters.push("*".into()); } for argument in &function.arguments.keyword_only_arguments { - parameters.push(argument_stub(argument, modules_to_import)); + parameters.push(argument_stub(argument, imports)); } if let Some(argument) = &function.arguments.kwarg { parameters.push(format!( "**{}", - variable_length_argument_stub(argument, modules_to_import) + variable_length_argument_stub(argument, imports) )); } let mut buffer = String::new(); @@ -160,17 +178,17 @@ fn function_stubs(function: &Function, modules_to_import: &mut BTreeSet) buffer.push(')'); if let Some(returns) = &function.returns { buffer.push_str(" -> "); - buffer.push_str(annotation_stub(returns, modules_to_import)); + buffer.push_str(type_hint_stub(returns, imports)); } buffer.push_str(": ..."); buffer } -fn attribute_stubs(attribute: &Attribute, modules_to_import: &mut BTreeSet) -> String { +fn attribute_stubs(attribute: &Attribute, imports: &mut Imports) -> String { let mut output = attribute.name.clone(); if let Some(annotation) = &attribute.annotation { output.push_str(": "); - output.push_str(annotation_stub(annotation, modules_to_import)); + output.push_str(type_hint_stub(annotation, imports)); } if let Some(value) = &attribute.value { output.push_str(" = "); @@ -179,11 +197,11 @@ fn attribute_stubs(attribute: &Attribute, modules_to_import: &mut BTreeSet) -> String { +fn argument_stub(argument: &Argument, imports: &mut Imports) -> String { let mut output = argument.name.clone(); if let Some(annotation) = &argument.annotation { output.push_str(": "); - output.push_str(annotation_stub(annotation, modules_to_import)); + output.push_str(type_hint_stub(annotation, imports)); } if let Some(default_value) = &argument.default_value { output.push_str(if argument.annotation.is_some() { @@ -198,68 +216,70 @@ fn argument_stub(argument: &Argument, modules_to_import: &mut BTreeSet) fn variable_length_argument_stub( argument: &VariableLengthArgument, - modules_to_import: &mut BTreeSet, + imports: &mut Imports, ) -> String { let mut output = argument.name.clone(); if let Some(annotation) = &argument.annotation { output.push_str(": "); - output.push_str(annotation_stub(annotation, modules_to_import)); + output.push_str(type_hint_stub(annotation, imports)); } output } -fn annotation_stub<'a>(annotation: &'a str, modules_to_import: &mut BTreeSet) -> &'a str { - // We iterate on the annotation string - // If it starts with a Python path like foo.bar, we add the module name (here foo) to the import list - // and we skip after it - let mut i = 0; - while i < annotation.len() { - if let Some(path) = path_prefix(&annotation[i..]) { - // We found a path! - i += path.len(); - if let Some((module, _)) = path.rsplit_once('.') { - modules_to_import.insert(module.into()); - } - } - i += 1; +fn type_hint_stub<'a>(annotation: &'a TypeHint, imports: &mut Imports) -> &'a str { + for import in &annotation.imports { + imports.add(import); } - annotation + &annotation.annotation } -// If the input starts with a path like foo.bar, returns it -fn path_prefix(input: &str) -> Option<&str> { - let mut length = identifier_prefix(input)?.len(); - loop { - // We try to add another identifier to the path - let Some(remaining) = input[length..].strip_prefix('.') else { - break; - }; - let Some(id) = identifier_prefix(remaining) else { - break; - }; - length += id.len() + 1; - } - Some(&input[..length]) +/// Datastructure to deduplicate, validate and generate imports +struct Imports { + /// module -> names + imports: BTreeMap>, } -// If the input starts with an identifier like foo, returns it -fn identifier_prefix(input: &str) -> Option<&str> { - // We get the first char and validate it - let mut iter = input.chars(); - let first_char = iter.next()?; - if first_char != '_' && !is_xid_start(first_char) { - return None; +impl Imports { + fn new() -> Self { + Self { + imports: BTreeMap::new(), + } } - let mut length = first_char.len_utf8(); - // We add extra chars as much as we can - for c in iter { - if is_xid_continue(c) { - length += c.len_utf8(); - } else { - break; + + fn add(&mut self, import: &TypeHintImport) { + self.imports + .entry(import.module.clone()) + .or_default() + .insert(import.name.clone()); + } + + /// Remove all local import paths i.e. 'foo' and 'bar.foo' if the module is 'bar.foo' (encoded as name = 'foo' and parents = \['bar'\] + fn filter_for_module(&mut self, name: &str, parents: &[&str]) { + let mut local_import_path = name.to_string(); + self.imports.remove(name); + for parent in parents { + local_import_path = format!("{local_import_path}.{parent}"); + self.imports.remove(&local_import_path); } } - Some(&input[0..length]) + + fn to_lines(&self) -> Vec { + let mut lines = Vec::with_capacity(self.imports.len()); + for (module, names) in &self.imports { + let mut output = String::new(); + output.push_str("from "); + output.push_str(module); + output.push_str(" import "); + for (i, name) in names.iter().enumerate() { + if i > 0 { + output.push_str(", "); + } + output.push_str(name); + } + lines.push(output); + } + lines + } } #[cfg(test)] @@ -267,44 +287,6 @@ mod tests { use super::*; use crate::model::Arguments; - #[test] - fn annotation_stub_proper_imports() { - let mut modules_to_import = BTreeSet::new(); - - // Basic int - annotation_stub("int", &mut modules_to_import); - assert!(modules_to_import.is_empty()); - - // Simple path - annotation_stub("collections.abc.Iterable", &mut modules_to_import); - assert!(modules_to_import.contains("collections.abc")); - - // With underscore - annotation_stub("_foo._bar_baz", &mut modules_to_import); - assert!(modules_to_import.contains("_foo")); - - // Basic generic - annotation_stub("typing.List[int]", &mut modules_to_import); - assert!(modules_to_import.contains("typing")); - - // Complex generic - annotation_stub("typing.List[foo.Bar[int]]", &mut modules_to_import); - assert!(modules_to_import.contains("foo")); - - // Callable - annotation_stub( - "typing.Callable[[int, baz.Bar], bar.Baz[bool]]", - &mut modules_to_import, - ); - assert!(modules_to_import.contains("bar")); - assert!(modules_to_import.contains("baz")); - - // Union - annotation_stub("a.B | b.C", &mut modules_to_import); - assert!(modules_to_import.contains("a")); - assert!(modules_to_import.contains("b")); - } - #[test] fn function_stubs_with_variable_length() { let function = Function { @@ -328,18 +310,27 @@ mod tests { keyword_only_arguments: vec![Argument { name: "karg".into(), default_value: None, - annotation: Some("str".into()), + annotation: Some(TypeHint { + annotation: "str".into(), + imports: Vec::new(), + }), }], kwarg: Some(VariableLengthArgument { name: "kwarg".into(), - annotation: Some("str".into()), + annotation: Some(TypeHint { + annotation: "str".into(), + imports: Vec::new(), + }), }), }, - returns: Some("list[str]".into()), + returns: Some(TypeHint { + annotation: "list[str]".into(), + imports: Vec::new(), + }), }; assert_eq!( "def func(posonly, /, arg, *varargs, karg: str, **kwarg: str) -> list[str]: ...", - function_stubs(&function, &mut BTreeSet::new()) + function_stubs(&function, &mut Imports::new()) ) } @@ -363,7 +354,10 @@ mod tests { keyword_only_arguments: vec![Argument { name: "karg".into(), default_value: Some("\"foo\"".into()), - annotation: Some("str".into()), + annotation: Some(TypeHint { + annotation: "str".into(), + imports: Vec::new(), + }), }], kwarg: None, }, @@ -371,7 +365,7 @@ mod tests { }; assert_eq!( "def afunc(posonly=1, /, arg=True, *, karg: str = \"foo\"): ...", - function_stubs(&function, &mut BTreeSet::new()) + function_stubs(&function, &mut Imports::new()) ) } } diff --git a/pyo3-macros-backend/src/frompyobject.rs b/pyo3-macros-backend/src/frompyobject.rs index d3232e90777..79b8a75d1fb 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, ConcatenationBuilder}; +use crate::introspection::elide_lifetimes; use crate::utils::{self, Ctx}; use proc_macro2::TokenStream; use quote::{format_ident, quote, quote_spanned, ToTokens}; @@ -100,13 +100,14 @@ impl<'a> Enum<'a> { } #[cfg(feature = "experimental-inspect")] - fn write_input_type(&self, builder: &mut ConcatenationBuilder, ctx: &Ctx) { - for (i, var) in self.variants.iter().enumerate() { - if i > 0 { - builder.push_str(" | "); - } - var.write_input_type(builder, ctx); - } + fn input_type(&self, ctx: &Ctx) -> TokenStream { + let pyo3_crate_path = &ctx.pyo3_path; + let union = self + .variants + .iter() + .map(|var| var.input_type(ctx)) + .collect::>(); + quote! { #pyo3_crate_path::type_hint_union!(#(#union),*) } } } @@ -458,48 +459,45 @@ impl<'a> Container<'a> { } #[cfg(feature = "experimental-inspect")] - fn write_input_type(&self, builder: &mut ConcatenationBuilder, ctx: &Ctx) { + fn input_type(&self, ctx: &Ctx) -> TokenStream { + let pyo3_crate_path = &ctx.pyo3_path; match &self.ty { ContainerType::StructNewtype(_, from_py_with, ty) => { - Self::write_field_input_type(from_py_with, ty, builder, ctx); + Self::field_input_type(from_py_with, ty, ctx) } ContainerType::TupleNewtype(from_py_with, ty) => { - Self::write_field_input_type(from_py_with, ty, builder, ctx); + Self::field_input_type(from_py_with, ty, ctx) } ContainerType::Tuple(tups) => { - builder.push_str("tuple["); - for (i, TupleStructField { from_py_with, ty }) in tups.iter().enumerate() { - if i > 0 { - builder.push_str(", "); - } - Self::write_field_input_type(from_py_with, ty, builder, ctx); - } - builder.push_str("]"); + let elements = tups + .iter() + .map(|TupleStructField { from_py_with, ty }| { + Self::field_input_type(from_py_with, ty, ctx) + }) + .collect::>(); + quote! { #pyo3_crate_path::type_hint_subscript!(#pyo3_crate_path::type_hint!("tuple")#(, #elements)*) } } ContainerType::Struct(_) => { // TODO: implement using a Protocol? - builder.push_str("_typeshed.Incomplete") + quote! { #pyo3_crate_path::type_hint!("_typeshed", "Incomplete") } } } } #[cfg(feature = "experimental-inspect")] - fn write_field_input_type( + fn field_input_type( from_py_with: &Option, ty: &syn::Type, - builder: &mut ConcatenationBuilder, ctx: &Ctx, - ) { + ) -> TokenStream { + let pyo3_crate_path = &ctx.pyo3_path; if from_py_with.is_some() { // We don't know what from_py_with is doing - builder.push_str("_typeshed.Incomplete") + quote! { #pyo3_crate_path::type_hint!("_typeshed", "Incomplete") } } else { let mut ty = ty.clone(); elide_lifetimes(&mut ty); - let pyo3_crate_path = &ctx.pyo3_path; - builder.push_tokens( - quote! { <#ty as #pyo3_crate_path::FromPyObject<'_, '_>>::INPUT_TYPE.as_bytes() }, - ) + quote! { <#ty as #pyo3_crate_path::FromPyObject<'_, '_>>::INPUT_TYPE } } } } @@ -569,34 +567,31 @@ pub fn build_derive_from_pyobject(tokens: &DeriveInput) -> Result { #[cfg(feature = "experimental-inspect")] let input_type = { - let mut builder = ConcatenationBuilder::default(); - if tokens + let pyo3_crate_path = &ctx.pyo3_path; + let input_type = if tokens .generics .params .iter() .all(|p| matches!(p, syn::GenericParam::Lifetime(_))) { match &tokens.data { - syn::Data::Enum(en) => { - Enum::new(en, &tokens.ident, options)?.write_input_type(&mut builder, ctx) - } + syn::Data::Enum(en) => Enum::new(en, &tokens.ident, options)?.input_type(ctx), syn::Data::Struct(st) => { let ident = &tokens.ident; Container::new(&st.fields, parse_quote!(#ident), options.clone())? - .write_input_type(&mut builder, ctx) + .input_type(ctx) } syn::Data::Union(_) => { // Not supported at this point - builder.push_str("_typeshed.Incomplete") + quote! { #pyo3_crate_path::type_hint!("_typeshed", "Incomplete") } } } } else { // We don't know how to deal with generic parameters // Blocked by https://github.com/rust-lang/rust/issues/76560 - builder.push_str("_typeshed.Incomplete") + quote! { #pyo3_crate_path::type_hint!("_typeshed", "Incomplete") } }; - let input_type = builder.into_token_stream(&ctx.pyo3_path); - quote! { const INPUT_TYPE: &'static str = unsafe { ::std::str::from_utf8_unchecked(#input_type) }; } + quote! { const INPUT_TYPE: #pyo3_crate_path::inspect::TypeHint = #input_type; } }; #[cfg(not(feature = "experimental-inspect"))] let input_type = quote! {}; diff --git a/pyo3-macros-backend/src/intopyobject.rs b/pyo3-macros-backend/src/intopyobject.rs index a49aaaae81d..b7bbb928510 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, ConcatenationBuilder}; +use crate::introspection::elide_lifetimes; use crate::utils::{self, Ctx}; use proc_macro2::{Span, TokenStream}; use quote::{format_ident, quote, quote_spanned, ToTokens}; @@ -360,52 +360,47 @@ impl<'a, const REF: bool> Container<'a, REF> { } #[cfg(feature = "experimental-inspect")] - fn write_output_type(&self, builder: &mut ConcatenationBuilder, ctx: &Ctx) { + fn output_type(&self, ctx: &Ctx) -> TokenStream { + let pyo3_crate_path = &ctx.pyo3_path; match &self.ty { ContainerType::StructNewtype(field) | ContainerType::TupleNewtype(field) => { - Self::write_field_output_type(&None, &field.ty, builder, ctx); + Self::field_output_type(&None, &field.ty, ctx) } ContainerType::Tuple(tups) => { - builder.push_str("tuple["); - for ( - i, - TupleStructField { - into_py_with, - field, - }, - ) in tups.iter().enumerate() - { - if i > 0 { - builder.push_str(", "); - } - Self::write_field_output_type(into_py_with, &field.ty, builder, ctx); - } - builder.push_str("]"); + let elements = tups + .iter() + .map( + |TupleStructField { + into_py_with, + field, + }| { + Self::field_output_type(into_py_with, &field.ty, ctx) + }, + ) + .collect::>(); + quote! { #pyo3_crate_path::type_hint_subscript!(#pyo3_crate_path::type_hint!("tuple")#(, #elements)*) } } ContainerType::Struct(_) => { // TODO: implement using a Protocol? - builder.push_str("_typeshed.Incomplete") + quote! { #pyo3_crate_path::type_hint!("_typeshed", "Incomplete") } } } } #[cfg(feature = "experimental-inspect")] - fn write_field_output_type( + fn field_output_type( into_py_with: &Option, ty: &syn::Type, - builder: &mut ConcatenationBuilder, ctx: &Ctx, - ) { + ) -> TokenStream { + let pyo3_crate_path = &ctx.pyo3_path; if into_py_with.is_some() { // We don't know what into_py_with is doing - builder.push_str("_typeshed.Incomplete") + quote! { #pyo3_crate_path::type_hint!("_typeshed", "Incomplete") } } else { let mut ty = ty.clone(); elide_lifetimes(&mut ty); - let pyo3_crate_path = &ctx.pyo3_path; - builder.push_tokens( - quote! { <#ty as #pyo3_crate_path::IntoPyObject<'_>>::OUTPUT_TYPE.as_bytes() }, - ) + quote! { <#ty as #pyo3_crate_path::IntoPyObject<'_>>::OUTPUT_TYPE } } } } @@ -485,13 +480,14 @@ impl<'a, const REF: bool> Enum<'a, REF> { } #[cfg(feature = "experimental-inspect")] - fn write_output_type(&self, builder: &mut ConcatenationBuilder, ctx: &Ctx) { - for (i, var) in self.variants.iter().enumerate() { - if i > 0 { - builder.push_str(" | "); - } - var.write_output_type(builder, ctx); - } + fn output_type(&self, ctx: &Ctx) -> TokenStream { + let pyo3_crate_path = &ctx.pyo3_path; + let union = self + .variants + .iter() + .map(|var| var.output_type(ctx)) + .collect::>(); + quote! { #pyo3_crate_path::type_hint_union!(#(#union),*) } } } @@ -587,17 +583,15 @@ pub fn build_derive_into_pyobject(tokens: &DeriveInput) -> Resu #[cfg(feature = "experimental-inspect")] let output_type = { - let mut builder = ConcatenationBuilder::default(); - if tokens + let pyo3_crate_path = &ctx.pyo3_path; + let output_type = if tokens .generics .params .iter() .all(|p| matches!(p, syn::GenericParam::Lifetime(_))) { match &tokens.data { - syn::Data::Enum(en) => { - Enum::::new(en, &tokens.ident)?.write_output_type(&mut builder, ctx) - } + syn::Data::Enum(en) => Enum::::new(en, &tokens.ident)?.output_type(ctx), syn::Data::Struct(st) => { let ident = &tokens.ident; Container::::new( @@ -606,20 +600,19 @@ pub fn build_derive_into_pyobject(tokens: &DeriveInput) -> Resu parse_quote!(#ident), options, )? - .write_output_type(&mut builder, ctx) + .output_type(ctx) } syn::Data::Union(_) => { // Not supported at this point - builder.push_str("_typeshed.Incomplete") + quote! { #pyo3_crate_path::type_hint!("_typeshed", "Incomplete") } } } } else { // We don't know how to deal with generic parameters // Blocked by https://github.com/rust-lang/rust/issues/76560 - builder.push_str("_typeshed.Incomplete") + quote! { #pyo3_crate_path::type_hint!("_typeshed", "Incomplete") } }; - let output_type = builder.into_token_stream(&ctx.pyo3_path); - quote! { const OUTPUT_TYPE: &'static str = unsafe { ::std::str::from_utf8_unchecked(#output_type) }; } + quote! { const OUTPUT_TYPE: #pyo3_path::inspect::TypeHint = #output_type; } }; #[cfg(not(feature = "experimental-inspect"))] let output_type = quote! {}; diff --git a/pyo3-macros-backend/src/introspection.rs b/pyo3-macros-backend/src/introspection.rs index 8ca21beedbe..6c9382f9533 100644 --- a/pyo3-macros-backend/src/introspection.rs +++ b/pyo3-macros-backend/src/introspection.rs @@ -104,11 +104,17 @@ pub fn function_introspection_code( IntrospectionNode::String(returns.to_python().into()) } else { match returns { - ReturnType::Default => IntrospectionNode::String("None".into()), + ReturnType::Default => IntrospectionNode::ConstantType { + name: "None", + module: None, + }, ReturnType::Type(_, ty) => match *ty { Type::Tuple(t) if t.elems.is_empty() => { // () is converted to None in return types - IntrospectionNode::String("None".into()) + IntrospectionNode::ConstantType { + name: "None", + module: None, + } } mut ty => { if let Some(class_type) = parent { @@ -183,7 +189,10 @@ 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]] - IntrospectionNode::String("typing.Final".into()) + IntrospectionNode::ConstantType { + name: "Final", + module: Some("typing"), + } } else { IntrospectionNode::OutputType { rust_type, @@ -343,8 +352,18 @@ enum IntrospectionNode<'a> { String(Cow<'a, str>), Bool(bool), IntrospectionId(Option>), - InputType { rust_type: Type, nullable: bool }, - OutputType { rust_type: Type, is_final: bool }, + InputType { + rust_type: Type, + nullable: bool, + }, + OutputType { + rust_type: Type, + is_final: bool, + }, + ConstantType { + name: &'static str, + module: Option<&'static str>, + }, Map(HashMap<&'static str, IntrospectionNode<'a>>), List(Vec>), } @@ -382,34 +401,37 @@ impl IntrospectionNode<'_> { rust_type, nullable, } => { - content.push_str("\""); - content.push_tokens(quote! { + let mut annotation = quote! { <#rust_type as #pyo3_crate_path::impl_::extract_argument::PyFunctionArgument< { #[allow(unused_imports)] use #pyo3_crate_path::impl_::pyclass::Probe as _; #pyo3_crate_path::impl_::pyclass::IsFromPyObject::<#rust_type>::VALUE } - >>::INPUT_TYPE.as_bytes() - }); + >>::INPUT_TYPE + }; if nullable { - content.push_str(" | None"); + annotation = quote! { #pyo3_crate_path::type_hint_union!(#annotation, #pyo3_crate_path::type_hint!("None")) }; } - content.push_str("\""); + content.push_tokens(quote! { #pyo3_crate_path::type_hint_json!(#annotation) }); } Self::OutputType { rust_type, is_final, } => { - content.push_str("\""); - if is_final { - content.push_str("typing.Final["); - } - content.push_tokens(quote! { <#rust_type as #pyo3_crate_path::impl_::introspection::PyReturnType>::OUTPUT_TYPE.as_bytes() }); + let mut annotation = quote! { <#rust_type as #pyo3_crate_path::impl_::introspection::PyReturnType>::OUTPUT_TYPE }; if is_final { - content.push_str("]"); + annotation = quote! { #pyo3_crate_path::type_hint_subscript!(#pyo3_crate_path::type_hint!("typing", "Final"), #annotation) }; } - content.push_str("\""); + content.push_tokens(quote! { #pyo3_crate_path::type_hint_json!(#annotation) }); + } + Self::ConstantType { name, module } => { + let annotation = if let Some(module) = module { + quote! { #pyo3_crate_path::type_hint!(#module, #name) } + } else { + quote! { #pyo3_crate_path::type_hint!(#name) } + }; + content.push_tokens(quote! { #pyo3_crate_path::type_hint_json!(#annotation) }); } Self::Map(map) => { content.push_str("{"); diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 0f8eea038d9..e26eece1e89 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -413,13 +413,15 @@ fn get_class_python_name<'a>(cls: &'a syn::Ident, args: &'a PyClassArgs) -> Cow< .unwrap_or_else(|| Cow::Owned(cls.unraw())) } -fn get_class_python_module_and_name<'a>(cls: &'a Ident, args: &'a PyClassArgs) -> String { - let name = get_class_python_name(cls, args); +#[cfg(feature = "experimental-inspect")] +fn get_class_type_hint(cls: &Ident, args: &PyClassArgs, ctx: &Ctx) -> TokenStream { + let pyo3_path = &ctx.pyo3_path; + let name = get_class_python_name(cls, args).to_string(); if let Some(module) = &args.options.module { - let value = module.value.value(); - format!("{value}.{name}") + let module = module.value.value(); + quote! { #pyo3_path::type_hint!(#module, #name) } } else { - name.to_string() + quote! { #pyo3_path::type_hint!(#name) } } } @@ -1094,12 +1096,13 @@ fn impl_complex_enum( } } }); - let output_type = if cfg!(feature = "experimental-inspect") { - let full_name = get_class_python_module_and_name(cls, &args); - quote! { const OUTPUT_TYPE: &'static str = #full_name; } - } else { - quote! {} + #[cfg(feature = "experimental-inspect")] + let output_type = { + let type_hint = get_class_type_hint(cls, &args, ctx); + quote! { const OUTPUT_TYPE: #pyo3_path::inspect::TypeHint = #type_hint; } }; + #[cfg(not(feature = "experimental-inspect"))] + let output_type = quote! {}; quote! { impl<'py> #pyo3_path::conversion::IntoPyObject<'py> for #cls { type Target = Self; @@ -1938,19 +1941,20 @@ fn impl_pytypeinfo(cls: &syn::Ident, attr: &PyClassArgs, ctx: &Ctx) -> TokenStre quote! { ::core::option::Option::None } }; - let python_type = if cfg!(feature = "experimental-inspect") { - let full_name = get_class_python_module_and_name(cls, attr); - quote! { const PYTHON_TYPE: &'static str = #full_name; } - } else { - quote! {} + #[cfg(feature = "experimental-inspect")] + let type_hint = { + let type_hint = get_class_type_hint(cls, attr, ctx); + quote! { const TYPE_HINT: #pyo3_path::inspect::TypeHint = #type_hint; } }; + #[cfg(not(feature = "experimental-inspect"))] + let type_hint = quote! {}; quote! { unsafe impl #pyo3_path::type_object::PyTypeInfo for #cls { const NAME: &'static str = #cls_name; const MODULE: ::std::option::Option<&'static str> = #module; - #python_type + #type_hint #[inline] fn type_object_raw(py: #pyo3_path::Python<'_>) -> *mut #pyo3_path::ffi::PyTypeObject { @@ -2326,12 +2330,13 @@ impl<'a> PyClassImplsBuilder<'a> { let attr = self.attr; // If #cls is not extended type, we allow Self->PyObject conversion if attr.options.extends.is_none() { - let output_type = if cfg!(feature = "experimental-inspect") { - let full_name = get_class_python_module_and_name(cls, self.attr); - quote! { const OUTPUT_TYPE: &'static str = #full_name; } - } else { - quote! {} + #[cfg(feature = "experimental-inspect")] + let output_type = { + let type_hint = get_class_type_hint(cls, attr, ctx); + quote! { const OUTPUT_TYPE: #pyo3_path::inspect::TypeHint = #type_hint; } }; + #[cfg(not(feature = "experimental-inspect"))] + let output_type = quote! {}; quote! { impl<'py> #pyo3_path::conversion::IntoPyObject<'py> for #cls { type Target = Self; @@ -2490,13 +2495,6 @@ impl<'a> PyClassImplsBuilder<'a> { } }; - let type_name = if cfg!(feature = "experimental-inspect") { - let full_name = get_class_python_module_and_name(cls, self.attr); - quote! { const TYPE_NAME: &'static str = #full_name; } - } else { - quote! {} - }; - Ok(quote! { #assertions @@ -2517,8 +2515,6 @@ impl<'a> PyClassImplsBuilder<'a> { type WeakRef = #weakref; type BaseNativeType = #base_nativetype; - #type_name - fn items_iter() -> #pyo3_path::impl_::pyclass::PyClassItemsIter { use #pyo3_path::impl_::pyclass::*; let collector = PyClassImplCollector::::new(); diff --git a/pytests/stubs/__init__.pyi b/pytests/stubs/__init__.pyi index b88c3a5f3c3..0f6820f054e 100644 --- a/pytests/stubs/__init__.pyi +++ b/pytests/stubs/__init__.pyi @@ -1,3 +1,3 @@ -import _typeshed +from _typeshed import Incomplete -def __getattr__(name: str) -> _typeshed.Incomplete: ... +def __getattr__(name: str) -> Incomplete: ... diff --git a/pytests/stubs/consts.pyi b/pytests/stubs/consts.pyi index 45c1d5fcbfb..66b0672c8a5 100644 --- a/pytests/stubs/consts.pyi +++ b/pytests/stubs/consts.pyi @@ -1,8 +1,7 @@ -import consts -import typing +from typing import Final -PI: typing.Final[float] -SIMPLE: typing.Final = "SIMPLE" +PI: Final[float] +SIMPLE: Final = "SIMPLE" class ClassWithConst: - INSTANCE: typing.Final[consts.ClassWithConst] + INSTANCE: Final[ClassWithConst] diff --git a/pytests/stubs/pyclasses.pyi b/pytests/stubs/pyclasses.pyi index 7f7ee521452..e2d1f4d006b 100644 --- a/pytests/stubs/pyclasses.pyi +++ b/pytests/stubs/pyclasses.pyi @@ -1,8 +1,8 @@ -import _typeshed -import typing +from _typeshed import Incomplete +from typing import Any class AssertingBaseClass: - def __new__(cls, /, expected_type: typing.Any) -> None: ... + def __new__(cls, /, expected_type: Any) -> None: ... class ClassWithDecorators: def __new__(cls, /) -> None: ... @@ -47,5 +47,5 @@ class PyClassThreadIter: def __next__(self, /) -> int: ... def map_a_class( - cls: EmptyClass | tuple[EmptyClass, EmptyClass] | _typeshed.Incomplete, -) -> EmptyClass | tuple[EmptyClass, EmptyClass] | _typeshed.Incomplete: ... + cls: EmptyClass | tuple[EmptyClass, EmptyClass] | Incomplete, +) -> EmptyClass | tuple[EmptyClass, EmptyClass] | Incomplete: ... diff --git a/pytests/stubs/pyfunctions.pyi b/pytests/stubs/pyfunctions.pyi index 369119cd96f..9513072d023 100644 --- a/pytests/stubs/pyfunctions.pyi +++ b/pytests/stubs/pyfunctions.pyi @@ -1,46 +1,38 @@ -import typing +from typing import Any -def args_kwargs(*args, **kwargs) -> typing.Any: ... +def args_kwargs(*args, **kwargs) -> Any: ... def many_keyword_arguments( *, - ant: typing.Any | None = None, - bear: typing.Any | None = None, - cat: typing.Any | None = None, - dog: typing.Any | None = None, - elephant: typing.Any | None = None, - fox: typing.Any | None = None, - goat: typing.Any | None = None, - horse: typing.Any | None = None, - iguana: typing.Any | None = None, - jaguar: typing.Any | None = None, - koala: typing.Any | None = None, - lion: typing.Any | None = None, - monkey: typing.Any | None = None, - newt: typing.Any | None = None, - owl: typing.Any | None = None, - penguin: typing.Any | None = None, + ant: Any | None = None, + bear: Any | None = None, + cat: Any | None = None, + dog: Any | None = None, + elephant: Any | None = None, + fox: Any | None = None, + goat: Any | None = None, + horse: Any | None = None, + iguana: Any | None = None, + jaguar: Any | None = None, + koala: Any | None = None, + lion: Any | None = None, + monkey: Any | None = None, + newt: Any | None = None, + owl: Any | None = None, + penguin: Any | None = None, ) -> None: ... def none() -> None: ... -def positional_only(a: typing.Any, /, b: typing.Any) -> typing.Any: ... -def simple( - a: typing.Any, b: typing.Any | None = None, *, c: typing.Any | None = None -) -> typing.Any: ... -def simple_args( - a: typing.Any, b: typing.Any | None = None, *args, c: typing.Any | None = None -) -> typing.Any: ... +def positional_only(a: Any, /, b: Any) -> Any: ... +def simple(a: Any, b: Any | None = None, *, c: Any | None = None) -> Any: ... +def simple_args(a: Any, b: Any | None = None, *args, c: Any | None = None) -> Any: ... def simple_args_kwargs( - a: typing.Any, - b: typing.Any | None = None, - *args, - c: typing.Any | None = None, - **kwargs, -) -> typing.Any: ... + a: Any, b: Any | None = None, *args, c: Any | None = None, **kwargs +) -> Any: ... def simple_kwargs( - a: typing.Any, b: typing.Any | None = None, c: typing.Any | None = None, **kwargs -) -> typing.Any: ... + a: Any, b: Any | None = None, c: Any | None = None, **kwargs +) -> Any: ... def with_custom_type_annotations( a: int, *_args: str, _b: int | None = None, **_kwargs: bool ) -> int: ... def with_typed_args( a: bool = False, b: int = 0, c: float = 0.0, d: str = "" -) -> typing.Any: ... +) -> Any: ... diff --git a/src/conversion.rs b/src/conversion.rs index 8b829d21aed..4035484b761 100644 --- a/src/conversion.rs +++ b/src/conversion.rs @@ -2,8 +2,12 @@ use crate::err::PyResult; #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; +#[cfg(feature = "experimental-inspect")] +use crate::inspect::TypeHint; use crate::pyclass::boolean_struct::False; use crate::pyclass::{PyClassGuardError, PyClassGuardMutError}; +#[cfg(feature = "experimental-inspect")] +use crate::type_hint; use crate::types::PyTuple; use crate::{ Borrowed, Bound, BoundObject, Py, PyAny, PyClass, PyClassGuard, PyErr, PyRef, PyRefMut, Python, @@ -59,7 +63,7 @@ pub trait IntoPyObject<'py>: Sized { /// For most types, the return value for this method will be identical to that of [`FromPyObject::INPUT_TYPE`]. /// It may be different for some types, such as `Dict`, to allow duck-typing: functions return `Dict` but take `Mapping` as argument. #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = "typing.Any"; + const OUTPUT_TYPE: TypeHint = type_hint!("typing", "Any"); /// Performs the conversion. fn into_pyobject(self, py: Python<'py>) -> Result; @@ -193,7 +197,7 @@ where type Error = <&'a T as IntoPyObject<'py>>::Error; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = <&'a T as IntoPyObject<'py>>::OUTPUT_TYPE; + const OUTPUT_TYPE: TypeHint = <&'a T as IntoPyObject<'py>>::OUTPUT_TYPE; #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { @@ -390,7 +394,7 @@ pub trait FromPyObject<'a, 'py>: Sized { /// 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"; + const INPUT_TYPE: TypeHint = type_hint!("typing", "Any"); /// Extracts `Self` from the bound smart pointer `obj`. /// @@ -534,7 +538,7 @@ where type Error = PyClassGuardError<'a, 'py>; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: &'static str = ::TYPE_NAME; + const INPUT_TYPE: TypeHint = ::TYPE_HINT; fn extract(obj: Borrowed<'a, 'py, PyAny>) -> Result { Ok(obj.extract::>()?.clone()) @@ -548,7 +552,7 @@ where type Error = PyClassGuardError<'a, 'py>; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: &'static str = ::TYPE_NAME; + const INPUT_TYPE: TypeHint = ::TYPE_HINT; fn extract(obj: Borrowed<'a, 'py, PyAny>) -> Result { obj.cast::() @@ -565,7 +569,7 @@ where type Error = PyClassGuardMutError<'a, 'py>; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: &'static str = ::TYPE_NAME; + const INPUT_TYPE: TypeHint = ::TYPE_HINT; fn extract(obj: Borrowed<'a, 'py, PyAny>) -> Result { obj.cast::() diff --git a/src/conversions/std/cell.rs b/src/conversions/std/cell.rs index 108df8031cd..9a79479ad6f 100644 --- a/src/conversions/std/cell.rs +++ b/src/conversions/std/cell.rs @@ -1,5 +1,7 @@ use std::cell::Cell; +#[cfg(feature = "experimental-inspect")] +use crate::inspect::TypeHint; use crate::{conversion::IntoPyObject, Borrowed, FromPyObject, PyAny, Python}; impl<'py, T: Copy + IntoPyObject<'py>> IntoPyObject<'py> for Cell { @@ -8,7 +10,7 @@ impl<'py, T: Copy + IntoPyObject<'py>> IntoPyObject<'py> for Cell { type Error = T::Error; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = T::OUTPUT_TYPE; + const OUTPUT_TYPE: TypeHint = T::OUTPUT_TYPE; #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { @@ -22,7 +24,7 @@ impl<'py, T: Copy + IntoPyObject<'py>> IntoPyObject<'py> for &Cell { type Error = T::Error; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = T::OUTPUT_TYPE; + const OUTPUT_TYPE: TypeHint = T::OUTPUT_TYPE; #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { @@ -34,7 +36,7 @@ impl<'a, 'py, T: FromPyObject<'a, 'py>> FromPyObject<'a, 'py> for Cell { type Error = T::Error; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: &'static str = T::INPUT_TYPE; + const INPUT_TYPE: TypeHint = T::INPUT_TYPE; fn extract(ob: Borrowed<'a, 'py, PyAny>) -> Result { ob.extract().map(Cell::new) diff --git a/src/conversions/std/num.rs b/src/conversions/std/num.rs index 80517d5c0e5..881f6bce2a6 100644 --- a/src/conversions/std/num.rs +++ b/src/conversions/std/num.rs @@ -3,6 +3,10 @@ use crate::conversion::{FromPyObjectSequence, IntoPyObject}; use crate::ffi_ptr_ext::FfiPtrExt; #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; +#[cfg(feature = "experimental-inspect")] +use crate::inspect::TypeHint; +#[cfg(feature = "experimental-inspect")] +use crate::type_hint; use crate::types::{PyByteArray, PyByteArrayMethods, PyBytes, PyInt}; use crate::{exceptions, ffi, Borrowed, Bound, FromPyObject, PyAny, PyErr, PyResult, Python}; use std::convert::Infallible; @@ -23,7 +27,7 @@ macro_rules! int_fits_larger_int { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = <$larger_type>::OUTPUT_TYPE; + const OUTPUT_TYPE: TypeHint = <$larger_type>::OUTPUT_TYPE; fn into_pyobject(self, py: Python<'py>) -> Result { (self as $larger_type).into_pyobject(py) @@ -41,7 +45,7 @@ macro_rules! int_fits_larger_int { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = <$larger_type>::OUTPUT_TYPE; + const OUTPUT_TYPE: TypeHint = <$larger_type>::OUTPUT_TYPE; fn into_pyobject(self, py: Python<'py>) -> Result { (*self).into_pyobject(py) @@ -57,7 +61,7 @@ macro_rules! int_fits_larger_int { type Error = PyErr; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: &'static str = <$larger_type>::INPUT_TYPE; + const INPUT_TYPE: TypeHint = <$larger_type>::INPUT_TYPE; fn extract(obj: Borrowed<'_, '_, PyAny>) -> Result { let val: $larger_type = obj.extract()?; @@ -106,7 +110,7 @@ macro_rules! int_convert_u64_or_i64 { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = "int"; + const OUTPUT_TYPE: TypeHint = type_hint!("int"); fn into_pyobject(self, py: Python<'py>) -> Result { unsafe { @@ -127,7 +131,7 @@ macro_rules! int_convert_u64_or_i64 { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = "int"; + const OUTPUT_TYPE: TypeHint = <$rust_type>::OUTPUT_TYPE; #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { @@ -138,7 +142,7 @@ macro_rules! int_convert_u64_or_i64 { type Error = PyErr; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: &'static str = "int"; + const INPUT_TYPE: TypeHint = type_hint!("int"); fn extract(obj: Borrowed<'_, '_, PyAny>) -> Result<$rust_type, Self::Error> { extract_int!(obj, !0, $pylong_as_ll_or_ull, $force_index_call) @@ -160,7 +164,7 @@ macro_rules! int_fits_c_long { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = "int"; + const OUTPUT_TYPE: TypeHint = type_hint!("int"); fn into_pyobject(self, py: Python<'py>) -> Result { unsafe { @@ -182,7 +186,7 @@ macro_rules! int_fits_c_long { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = "int"; + const OUTPUT_TYPE: TypeHint = <$rust_type>::OUTPUT_TYPE; #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { @@ -199,7 +203,7 @@ macro_rules! int_fits_c_long { type Error = PyErr; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: &'static str = "int"; + const INPUT_TYPE: TypeHint = type_hint!("int"); fn extract(obj: Borrowed<'_, 'py, PyAny>) -> Result { let val: c_long = extract_int!(obj, -1, ffi::PyLong_AsLong)?; @@ -221,7 +225,7 @@ impl<'py> IntoPyObject<'py> for u8 { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = "int"; + const OUTPUT_TYPE: TypeHint = type_hint!("int"); fn into_pyobject(self, py: Python<'py>) -> Result { unsafe { @@ -255,7 +259,7 @@ impl<'py> IntoPyObject<'py> for &'_ u8 { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = "int"; + const OUTPUT_TYPE: TypeHint = u8::OUTPUT_TYPE; fn into_pyobject(self, py: Python<'py>) -> Result { u8::into_pyobject(*self, py) @@ -284,7 +288,7 @@ impl<'py> FromPyObject<'_, 'py> for u8 { type Error = PyErr; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: &'static str = "int"; + const INPUT_TYPE: TypeHint = type_hint!("int"); fn extract(obj: Borrowed<'_, 'py, PyAny>) -> Result { let val: c_long = extract_int!(obj, -1, ffi::PyLong_AsLong)?; @@ -432,7 +436,7 @@ mod fast_128bit_int_conversion { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = "int"; + const OUTPUT_TYPE: TypeHint = type_hint!("int"); fn into_pyobject(self, py: Python<'py>) -> Result { #[cfg(not(Py_3_13))] @@ -489,7 +493,7 @@ mod fast_128bit_int_conversion { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = "int"; + const OUTPUT_TYPE: TypeHint = <$rust_type>::OUTPUT_TYPE; #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { @@ -506,7 +510,7 @@ mod fast_128bit_int_conversion { type Error = PyErr; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: &'static str = "int"; + const INPUT_TYPE: TypeHint = type_hint!("int"); fn extract(ob: Borrowed<'_, '_, PyAny>) -> Result<$rust_type, Self::Error> { let num = @@ -582,7 +586,7 @@ mod slow_128bit_int_conversion { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = "int"; + const OUTPUT_TYPE: TypeHint = type_hint!("int"); fn into_pyobject(self, py: Python<'py>) -> Result { let lower = (self as u64).into_pyobject(py)?; @@ -610,7 +614,7 @@ mod slow_128bit_int_conversion { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = "int"; + const OUTPUT_TYPE: TypeHint = <$rust_type>::OUTPUT_TYPE; #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { @@ -627,7 +631,7 @@ mod slow_128bit_int_conversion { type Error = PyErr; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: &'static str = "int"; + const INPUT_TYPE: TypeHint = type_hint!("int"); fn extract(ob: Borrowed<'_, '_, PyAny>) -> Result<$rust_type, Self::Error> { let py = ob.py(); @@ -681,7 +685,7 @@ macro_rules! nonzero_int_impl { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = "int"; + const OUTPUT_TYPE: TypeHint = type_hint!("int"); #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { @@ -700,7 +704,7 @@ macro_rules! nonzero_int_impl { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = "int"; + const OUTPUT_TYPE: TypeHint = <$nonzero_type>::OUTPUT_TYPE; #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { @@ -717,7 +721,7 @@ macro_rules! nonzero_int_impl { type Error = PyErr; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: &'static str = <$primitive_type>::INPUT_TYPE; + const INPUT_TYPE: TypeHint = <$primitive_type>::INPUT_TYPE; fn extract(obj: Borrowed<'_, '_, PyAny>) -> Result { let val: $primitive_type = obj.extract()?; diff --git a/src/conversions/std/string.rs b/src/conversions/std/string.rs index 3c6a300ecdf..92717f13950 100644 --- a/src/conversions/std/string.rs +++ b/src/conversions/std/string.rs @@ -1,11 +1,14 @@ -use std::{borrow::Cow, convert::Infallible}; - #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; +#[cfg(feature = "experimental-inspect")] +use crate::inspect::TypeHint; +#[cfg(feature = "experimental-inspect")] +use crate::type_hint; use crate::{ conversion::IntoPyObject, instance::Bound, types::PyString, Borrowed, FromPyObject, PyAny, PyErr, Python, }; +use std::{borrow::Cow, convert::Infallible}; impl<'py> IntoPyObject<'py> for &str { type Target = PyString; @@ -13,7 +16,7 @@ impl<'py> IntoPyObject<'py> for &str { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = String::OUTPUT_TYPE; + const OUTPUT_TYPE: TypeHint = String::OUTPUT_TYPE; #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { @@ -32,7 +35,7 @@ impl<'py> IntoPyObject<'py> for &&str { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = String::OUTPUT_TYPE; + const OUTPUT_TYPE: TypeHint = String::OUTPUT_TYPE; #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { @@ -51,7 +54,7 @@ impl<'py> IntoPyObject<'py> for Cow<'_, str> { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = String::OUTPUT_TYPE; + const OUTPUT_TYPE: TypeHint = String::OUTPUT_TYPE; #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { @@ -70,7 +73,7 @@ impl<'py> IntoPyObject<'py> for &Cow<'_, str> { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = String::OUTPUT_TYPE; + const OUTPUT_TYPE: TypeHint = String::OUTPUT_TYPE; #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { @@ -89,7 +92,7 @@ impl<'py> IntoPyObject<'py> for char { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = String::OUTPUT_TYPE; + const OUTPUT_TYPE: TypeHint = String::OUTPUT_TYPE; fn into_pyobject(self, py: Python<'py>) -> Result { let mut bytes = [0u8; 4]; @@ -108,7 +111,7 @@ impl<'py> IntoPyObject<'py> for &char { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = String::OUTPUT_TYPE; + const OUTPUT_TYPE: TypeHint = String::OUTPUT_TYPE; #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { @@ -127,7 +130,7 @@ impl<'py> IntoPyObject<'py> for String { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = "str"; + const OUTPUT_TYPE: TypeHint = type_hint!("str"); fn into_pyobject(self, py: Python<'py>) -> Result { Ok(PyString::new(py, &self)) @@ -145,7 +148,7 @@ impl<'py> IntoPyObject<'py> for &String { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = String::OUTPUT_TYPE; + const OUTPUT_TYPE: TypeHint = String::OUTPUT_TYPE; #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { @@ -163,7 +166,7 @@ impl<'a> crate::conversion::FromPyObject<'a, '_> for &'a str { type Error = PyErr; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: &'static str = "str"; + const INPUT_TYPE: TypeHint = type_hint!("str"); fn extract(ob: crate::Borrowed<'a, '_, PyAny>) -> Result { ob.cast::()?.to_str() @@ -179,7 +182,7 @@ impl<'a> crate::conversion::FromPyObject<'a, '_> for Cow<'a, str> { type Error = PyErr; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: &'static str = "str"; + const INPUT_TYPE: TypeHint = type_hint!("str"); fn extract(ob: crate::Borrowed<'a, '_, PyAny>) -> Result { ob.cast::()?.to_cow() @@ -197,7 +200,7 @@ impl FromPyObject<'_, '_> for String { type Error = PyErr; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: &'static str = "str"; + const INPUT_TYPE: TypeHint = type_hint!("str"); fn extract(obj: Borrowed<'_, '_, PyAny>) -> Result { obj.cast::()?.to_cow().map(Cow::into_owned) @@ -213,7 +216,7 @@ impl FromPyObject<'_, '_> for char { type Error = PyErr; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: &'static str = "str"; + const INPUT_TYPE: TypeHint = type_hint!("str"); fn extract(obj: Borrowed<'_, '_, PyAny>) -> Result { let s = obj.cast::()?.to_cow()?; diff --git a/src/impl_/extract_argument.rs b/src/impl_/extract_argument.rs index 424b3ef998f..86ce118539f 100644 --- a/src/impl_/extract_argument.rs +++ b/src/impl_/extract_argument.rs @@ -1,3 +1,5 @@ +#[cfg(feature = "experimental-inspect")] +use crate::inspect::TypeHint; use crate::{ exceptions::PyTypeError, ffi, @@ -6,6 +8,8 @@ use crate::{ Borrowed, Bound, DowncastError, FromPyObject, PyAny, PyClass, PyClassGuard, PyClassGuardMut, PyErr, PyResult, PyTypeCheck, Python, }; +#[cfg(feature = "experimental-inspect")] +use crate::{type_hint, type_hint_union}; /// Helper type used to keep implementation more concise. /// @@ -60,7 +64,7 @@ pub trait PyFunctionArgument<'a, 'holder, 'py, const IMPLEMENTS_FROMPYOBJECT: bo /// Provides the type hint information for which Python types are allowed. #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: &'static str; + const INPUT_TYPE: TypeHint; fn extract( obj: &'a Bound<'py, PyAny>, @@ -76,7 +80,7 @@ where type Error = T::Error; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: &'static str = T::INPUT_TYPE; + const INPUT_TYPE: TypeHint = T::INPUT_TYPE; #[inline] fn extract(obj: &'a Bound<'py, PyAny>, _: &'_ mut ()) -> Result { @@ -92,7 +96,7 @@ where type Error = DowncastError<'a, 'py>; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: &'static str = T::PYTHON_TYPE; + const INPUT_TYPE: TypeHint = T::TYPE_HINT; #[inline] fn extract(obj: &'a Bound<'py, PyAny>, _: &'_ mut ()) -> Result { @@ -109,7 +113,7 @@ where type Error = T::Error; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: &'static str = "typing.Any | None"; + const INPUT_TYPE: TypeHint = type_hint_union!(type_hint!("typing", "Any"), type_hint!("None")); #[inline] fn extract( @@ -130,7 +134,7 @@ impl<'a, 'holder, 'py> PyFunctionArgument<'a, 'holder, 'py, false> for &'holder type Error = as FromPyObject<'a, 'py>>::Error; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: &'static str = "str"; + const INPUT_TYPE: TypeHint = type_hint!("str"); #[inline] fn extract( @@ -160,7 +164,7 @@ impl<'a, 'holder, T: PyClass> PyFunctionArgument<'a, 'holder, '_, false> for &'h type Error = PyErr; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: &'static str = T::PYTHON_TYPE; + const INPUT_TYPE: TypeHint = T::TYPE_HINT; #[inline] fn extract(obj: &'a Bound<'_, PyAny>, holder: &'holder mut Self::Holder) -> PyResult { @@ -175,7 +179,7 @@ impl<'a, 'holder, T: PyClass> PyFunctionArgument<'a, 'holder, '_ type Error = PyErr; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: &'static str = T::PYTHON_TYPE; + const INPUT_TYPE: TypeHint = T::TYPE_HINT; #[inline] fn extract(obj: &'a Bound<'_, PyAny>, holder: &'holder mut Self::Holder) -> PyResult { diff --git a/src/impl_/introspection.rs b/src/impl_/introspection.rs index a0f3ec81942..e6a2af940a5 100644 --- a/src/impl_/introspection.rs +++ b/src/impl_/introspection.rs @@ -1,19 +1,55 @@ use crate::conversion::IntoPyObject; +use crate::inspect::TypeHint; /// Trait to guess a function Python return type /// /// It is useful to properly get the return type `T` when the Rust implementation returns e.g. `PyResult` pub trait PyReturnType { /// The function return type - const OUTPUT_TYPE: &'static str; + const OUTPUT_TYPE: TypeHint; } impl<'a, T: IntoPyObject<'a>> PyReturnType for T { - const OUTPUT_TYPE: &'static str = T::OUTPUT_TYPE; + const OUTPUT_TYPE: TypeHint = T::OUTPUT_TYPE; } impl PyReturnType for Result { - const OUTPUT_TYPE: &'static str = T::OUTPUT_TYPE; + const OUTPUT_TYPE: TypeHint = T::OUTPUT_TYPE; +} + +// TODO: convert it in a macro to build the full JSON for the type hint +#[doc(hidden)] +#[macro_export] +macro_rules! type_hint_json { + ($hint:expr) => {{ + const HINT: $crate::inspect::TypeHint = $hint; + const PARTS_LEN: usize = 3 + 4 * HINT.imports.len(); + const PARTS: [&[u8]; PARTS_LEN] = { + let mut args: [&[u8]; PARTS_LEN] = [b""; PARTS_LEN]; + args[0] = b"{\"annotation\":\""; + args[1] = HINT.annotation.as_bytes(); + if HINT.imports.is_empty() { + args[2] = b"\",\"imports\":[]}" + } else { + args[2] = b"\",\"imports\":[{\"module\":\""; + let mut i = 0; + while i < HINT.imports.len() { + if i > 0 { + args[4 * i + 2] = b"\"},{\"module\":\""; + } + args[4 * i + 3] = HINT.imports[i].module.as_bytes(); + args[4 * i + 4] = b"\",\"name\":\""; + args[4 * i + 5] = HINT.imports[i].name.as_bytes(); + i += 1; + } + args[4 * i + 2] = b"\"}]}"; + } + args + }; + &$crate::impl_::concat::combine_to_array::<{ $crate::impl_::concat::combined_len(&PARTS) }>( + &PARTS, + ) + }}; } #[repr(C)] diff --git a/src/impl_/pyclass.rs b/src/impl_/pyclass.rs index 8950b2ffc95..8b830a5a5f5 100644 --- a/src/impl_/pyclass.rs +++ b/src/impl_/pyclass.rs @@ -219,9 +219,6 @@ pub trait PyClassImpl: Sized + 'static { /// from the PyClassDocGenerator` type. const DOC: &'static CStr; - #[cfg(feature = "experimental-inspect")] - const TYPE_NAME: &'static str; - fn items_iter() -> PyClassItemsIter; #[inline] diff --git a/src/inspect/mod.rs b/src/inspect/mod.rs index f14f8a2d2ae..feb89ab6448 100644 --- a/src/inspect/mod.rs +++ b/src/inspect/mod.rs @@ -1,4 +1,217 @@ //! Runtime inspection of objects exposed to Python. //! //! Tracking issue: . + +use std::fmt; +use std::fmt::Formatter; + pub mod types; + +/// A [type hint](https://docs.python.org/3/glossary.html#term-type-hint) with a list of imports to make it valid +/// +/// This struct aims at being used in `const` contexts like in [`FromPyObject::INPUT_TYPE`](crate::FromPyObject::INPUT_TYPE) and [`IntoPyObject::OUTPUT_TYPE`](crate::IntoPyObject::OUTPUT_TYPE). +/// +/// ``` +/// use pyo3::{type_hint, type_hint_union}; +/// use pyo3::inspect::TypeHint; +/// +/// const T: TypeHint = type_hint_union!(type_hint!("int"), type_hint!("b", "B")); +/// assert_eq!(T.to_string(), "int | B"); +/// ``` +#[derive(Clone, Copy)] +pub struct TypeHint { + /// The type hint annotation + #[doc(hidden)] + pub annotation: &'static str, + /// The modules to import + #[doc(hidden)] + pub imports: &'static [TypeHintImport], +} + +/// `from {module} import {name}` import +#[doc(hidden)] +#[derive(Clone, Copy)] +pub struct TypeHintImport { + /// The module from which to import + #[doc(hidden)] + pub module: &'static str, + /// The elements to import from the module + #[doc(hidden)] + pub name: &'static str, +} + +impl fmt::Display for TypeHint { + #[inline] + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + self.annotation.fmt(f) + } +} + +/// Allows to build a [`TypeHint`] from a module name and a qualified name +/// +/// ``` +/// use pyo3::type_hint; +/// use pyo3::inspect::TypeHint; +/// +/// const T: TypeHint = type_hint!("collections.abc", "Sequence"); +/// assert_eq!(T.to_string(), "Sequence"); +/// ``` +#[macro_export] +macro_rules! type_hint { + ($qualname: expr) => { + $crate::inspect::TypeHint { + annotation: $qualname, + imports: &[], + } + }; + ($module:expr, $name: expr) => { + $crate::inspect::TypeHint { + annotation: $name, + imports: &[$crate::inspect::TypeHintImport { + module: $module, + name: $name, + }], + } + }; +} + +/// Allows to build a [`TypeHint`] that is the union of other [`TypeHint`] +/// +/// ``` +/// use pyo3::{type_hint, type_hint_union}; +/// use pyo3::inspect::TypeHint; +/// +/// const T: TypeHint = type_hint_union!(type_hint!("a", "A"), type_hint!("b", "B")); +/// assert_eq!(T.to_string(), "A | B"); +/// ``` +#[macro_export] +macro_rules! type_hint_union { + // TODO: avoid using the parameters twice + // TODO: factor our common code in const functions + ($arg:expr) => { $arg }; + ($firstarg:expr, $($arg:expr),+) => {{ + $crate::inspect::TypeHint { + annotation: { + const PARTS: &[&[u8]] = &[$firstarg.annotation.as_bytes(), $(b" | ", $arg.annotation.as_bytes()),*]; + unsafe { + ::std::str::from_utf8_unchecked(&$crate::impl_::concat::combine_to_array::<{ + $crate::impl_::concat::combined_len(PARTS) + }>(PARTS)) + } + }, + imports: { + const ARGS: &[$crate::inspect::TypeHint] = &[$firstarg, $($arg),*]; + const LEN: usize = { + let mut count = 0; + let mut i = 0; + while i < ARGS.len() { + count += ARGS[i].imports.len(); + i += 1; + } + count + }; + const OUTPUT: [$crate::inspect::TypeHintImport; LEN] = { + let mut output = [$crate::inspect::TypeHintImport { module: "", name: "" }; LEN]; + let mut args_i = 0; + let mut in_arg_i = 0; + let mut output_i = 0; + while args_i < ARGS.len() { + while in_arg_i < ARGS[args_i].imports.len() { + output[output_i] = ARGS[args_i].imports[in_arg_i]; + in_arg_i += 1; + output_i += 1; + } + args_i += 1; + in_arg_i = 0; + } + output + }; + &OUTPUT + } + } + }}; +} + +/// Allows to build a [`TypeHint`] that is the subscripted +/// +/// ``` +/// use pyo3::{type_hint, type_hint_subscript}; +/// use pyo3::inspect::TypeHint; +/// +/// const T: TypeHint = type_hint_subscript!(type_hint!("collections.abc", "Sequence"), type_hint!("weakref", "ProxyType")); +/// assert_eq!(T.to_string(), "Sequence[ProxyType]"); +/// ``` +#[macro_export] +macro_rules! type_hint_subscript { + // TODO: avoid using the parameters twice + // TODO: factor our common code in const functions + ($main:expr, $firstarg:expr $(, $arg:expr)*) => {{ + $crate::inspect::TypeHint { + annotation: { + const PARTS: &[&[u8]] = &[$main.annotation.as_bytes(), b"[", $firstarg.annotation.as_bytes() $(, b", ", $arg.annotation.as_bytes())* , b"]"]; + unsafe { + ::std::str::from_utf8_unchecked(&$crate::impl_::concat::combine_to_array::<{ + $crate::impl_::concat::combined_len(PARTS) + }>(PARTS)) + } + }, + imports: { + const ARGS: &[$crate::inspect::TypeHint] = &[$main, $firstarg, $($arg),*]; + const LEN: usize = { + let mut count = 0; + let mut i = 0; + while i < ARGS.len() { + count += ARGS[i].imports.len(); + i += 1; + } + count + }; + const OUTPUT: [$crate::inspect::TypeHintImport; LEN] = { + let mut output = [$crate::inspect::TypeHintImport { module: "", name: "" }; LEN]; + let mut args_i = 0; + let mut in_arg_i = 0; + let mut output_i = 0; + while args_i < ARGS.len() { + while in_arg_i < ARGS[args_i].imports.len() { + output[output_i] = ARGS[args_i].imports[in_arg_i]; + in_arg_i += 1; + output_i += 1; + } + args_i += 1; + in_arg_i = 0; + } + output + }; + &OUTPUT + } + } + }}; +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_union() { + const T: TypeHint = type_hint_union!(type_hint!("a", "A"), type_hint!("b", "B")); + assert_eq!(T.annotation, "A | B"); + assert_eq!(T.imports[0].name, "A"); + assert_eq!(T.imports[0].module, "a"); + assert_eq!(T.imports[1].name, "B"); + assert_eq!(T.imports[1].module, "b"); + } + + #[test] + fn test_subscript() { + const T: TypeHint = type_hint_subscript!( + type_hint!("collections.abc", "Sequence"), + type_hint!("weakref", "ProxyType") + ); + assert_eq!(T.annotation, "Sequence[ProxyType]"); + assert_eq!(T.imports[0].name, "Sequence"); + assert_eq!(T.imports[0].module, "collections.abc"); + assert_eq!(T.imports[1].name, "ProxyType"); + assert_eq!(T.imports[1].module, "weakref"); + } +} diff --git a/src/instance.rs b/src/instance.rs index 3fc37cd0d41..a3ab67ae8f1 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -2,6 +2,8 @@ use crate::call::PyCallArgs; use crate::conversion::IntoPyObject; use crate::err::{self, PyErr, PyResult}; use crate::impl_::pycell::PyClassObject; +#[cfg(feature = "experimental-inspect")] +use crate::inspect::TypeHint; use crate::internal_tricks::ptr_from_ref; use crate::pycell::{PyBorrowError, PyBorrowMutError}; use crate::pyclass::boolean_struct::{False, True}; @@ -2041,7 +2043,7 @@ where type Error = DowncastError<'a, 'py>; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: &'static str = T::PYTHON_TYPE; + const INPUT_TYPE: TypeHint = T::TYPE_HINT; /// Extracts `Self` from the source `PyObject`. fn extract(ob: Borrowed<'a, 'py, PyAny>) -> Result { @@ -2056,7 +2058,7 @@ where type Error = DowncastError<'a, 'py>; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: &'static str = T::PYTHON_TYPE; + const INPUT_TYPE: TypeHint = T::TYPE_HINT; /// Extracts `Self` from the source `PyObject`. fn extract(ob: Borrowed<'a, 'py, PyAny>) -> Result { diff --git a/src/pycell.rs b/src/pycell.rs index c1f9606e40c..a9670279042 100644 --- a/src/pycell.rs +++ b/src/pycell.rs @@ -205,6 +205,8 @@ use std::mem::ManuallyDrop; use std::ops::{Deref, DerefMut}; pub(crate) mod impl_; +#[cfg(feature = "experimental-inspect")] +use crate::inspect::TypeHint; use impl_::{PyClassBorrowChecker, PyClassObjectLayout}; /// A wrapper type for an immutably borrowed value from a [`Bound<'py, T>`]. @@ -477,7 +479,7 @@ impl<'py, T: PyClass> IntoPyObject<'py> for PyRef<'py, T> { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = T::PYTHON_TYPE; + const OUTPUT_TYPE: TypeHint = T::TYPE_HINT; fn into_pyobject(self, _py: Python<'py>) -> Result { Ok(self.inner.clone()) @@ -490,7 +492,7 @@ impl<'a, 'py, T: PyClass> IntoPyObject<'py> for &'a PyRef<'py, T> { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = T::PYTHON_TYPE; + const OUTPUT_TYPE: TypeHint = T::TYPE_HINT; fn into_pyobject(self, _py: Python<'py>) -> Result { Ok(self.inner.as_borrowed()) @@ -654,7 +656,7 @@ impl<'py, T: PyClass> IntoPyObject<'py> for PyRefMut<'py, T> { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = T::PYTHON_TYPE; + const OUTPUT_TYPE: TypeHint = T::TYPE_HINT; fn into_pyobject(self, _py: Python<'py>) -> Result { Ok(self.inner.clone()) @@ -667,7 +669,7 @@ impl<'a, 'py, T: PyClass> IntoPyObject<'py> for &'a PyRefMut<'py type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = T::PYTHON_TYPE; + const OUTPUT_TYPE: TypeHint = T::TYPE_HINT; fn into_pyobject(self, _py: Python<'py>) -> Result { Ok(self.inner.as_borrowed()) diff --git a/src/pyclass/guard.rs b/src/pyclass/guard.rs index cc2b949e42e..7dd1a7fdb63 100644 --- a/src/pyclass/guard.rs +++ b/src/pyclass/guard.rs @@ -1,4 +1,6 @@ use crate::impl_::pycell::{PyClassObject, PyClassObjectLayout as _}; +#[cfg(feature = "experimental-inspect")] +use crate::inspect::TypeHint; use crate::pycell::PyBorrowMutError; use crate::pycell::{impl_::PyClassBorrowChecker, PyBorrowError}; use crate::pyclass::boolean_struct::False; @@ -307,7 +309,7 @@ impl<'a, 'py, T: PyClass> IntoPyObject<'py> for &PyClassGuard<'a, T> { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = T::PYTHON_TYPE; + const OUTPUT_TYPE: TypeHint = T::TYPE_HINT; #[inline] fn into_pyobject(self, py: crate::Python<'py>) -> Result { diff --git a/src/type_object.rs b/src/type_object.rs index f552ef5034e..f8fab527da9 100644 --- a/src/type_object.rs +++ b/src/type_object.rs @@ -1,6 +1,10 @@ //! Python type object information use crate::ffi_ptr_ext::FfiPtrExt; +#[cfg(feature = "experimental-inspect")] +use crate::inspect::TypeHint; +#[cfg(feature = "experimental-inspect")] +use crate::type_hint; use crate::types::{PyAny, PyType}; use crate::{ffi, Bound, Python}; use std::ptr; @@ -42,9 +46,9 @@ pub unsafe trait PyTypeInfo: Sized { /// Module name, if any. const MODULE: Option<&'static str>; - /// Provides the full python type paths. + /// Provides the full python type as a type hint. #[cfg(feature = "experimental-inspect")] - const PYTHON_TYPE: &'static str = "typing.Any"; + const TYPE_HINT: TypeHint = type_hint!("typing", "Any"); /// Returns the PyTypeObject instance for this type. fn type_object_raw(py: Python<'_>) -> *mut ffi::PyTypeObject; @@ -89,9 +93,9 @@ 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. + /// Provides the full python type of the allowed values as a Python type hint. #[cfg(feature = "experimental-inspect")] - const PYTHON_TYPE: &'static str; + const TYPE_HINT: TypeHint; /// Checks if `object` is an instance of `Self`, which may include a subtype. /// @@ -106,7 +110,7 @@ where const NAME: &'static str = ::NAME; #[cfg(feature = "experimental-inspect")] - const PYTHON_TYPE: &'static str = ::PYTHON_TYPE; + const TYPE_HINT: TypeHint = ::TYPE_HINT; #[inline] fn type_check(object: &Bound<'_, PyAny>) -> bool { diff --git a/src/types/boolobject.rs b/src/types/boolobject.rs index 454b6afb03c..518c0642b08 100644 --- a/src/types/boolobject.rs +++ b/src/types/boolobject.rs @@ -1,13 +1,16 @@ +use super::any::PyAnyMethods; +use crate::conversion::IntoPyObject; #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; +#[cfg(feature = "experimental-inspect")] +use crate::inspect::TypeHint; +#[cfg(feature = "experimental-inspect")] +use crate::type_hint; +use crate::PyErr; use crate::{ exceptions::PyTypeError, ffi, ffi_ptr_ext::FfiPtrExt, instance::Bound, types::typeobject::PyTypeMethods, Borrowed, FromPyObject, PyAny, Python, }; - -use super::any::PyAnyMethods; -use crate::conversion::IntoPyObject; -use crate::PyErr; use std::convert::Infallible; use std::ptr; @@ -143,7 +146,7 @@ impl<'py> IntoPyObject<'py> for bool { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = "bool"; + const OUTPUT_TYPE: TypeHint = type_hint!("bool"); #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { @@ -162,7 +165,7 @@ impl<'py> IntoPyObject<'py> for &bool { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = bool::OUTPUT_TYPE; + const OUTPUT_TYPE: TypeHint = bool::OUTPUT_TYPE; #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { @@ -182,7 +185,7 @@ impl FromPyObject<'_, '_> for bool { type Error = PyErr; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: &'static str = "bool"; + const INPUT_TYPE: TypeHint = type_hint!("bool"); fn extract(obj: Borrowed<'_, '_, PyAny>) -> Result { let err = match obj.cast::() { diff --git a/src/types/float.rs b/src/types/float.rs index 6fbe2d3679b..5c70a19f1a4 100644 --- a/src/types/float.rs +++ b/src/types/float.rs @@ -1,6 +1,10 @@ use crate::conversion::IntoPyObject; #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; +#[cfg(feature = "experimental-inspect")] +use crate::inspect::TypeHint; +#[cfg(feature = "experimental-inspect")] +use crate::type_hint; use crate::{ ffi, ffi_ptr_ext::FfiPtrExt, instance::Bound, Borrowed, FromPyObject, PyAny, PyErr, Python, }; @@ -73,7 +77,7 @@ impl<'py> IntoPyObject<'py> for f64 { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = "float"; + const OUTPUT_TYPE: TypeHint = type_hint!("float"); #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { @@ -92,7 +96,7 @@ impl<'py> IntoPyObject<'py> for &f64 { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = f64::OUTPUT_TYPE; + const OUTPUT_TYPE: TypeHint = f64::OUTPUT_TYPE; #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { @@ -109,7 +113,7 @@ impl<'py> FromPyObject<'_, 'py> for f64 { type Error = PyErr; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: &'static str = "float"; + const INPUT_TYPE: TypeHint = type_hint!("float"); // PyFloat_AsDouble returns -1.0 upon failure #[allow(clippy::float_cmp)] @@ -146,7 +150,7 @@ impl<'py> IntoPyObject<'py> for f32 { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = "float"; + const OUTPUT_TYPE: TypeHint = type_hint!("float"); #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { @@ -165,7 +169,7 @@ impl<'py> IntoPyObject<'py> for &f32 { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = f32::OUTPUT_TYPE; + const OUTPUT_TYPE: TypeHint = f32::OUTPUT_TYPE; #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { @@ -182,7 +186,7 @@ impl<'a, 'py> FromPyObject<'a, 'py> for f32 { type Error = >::Error; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: &'static str = "float"; + const INPUT_TYPE: TypeHint = type_hint!("float"); fn extract(obj: Borrowed<'_, 'py, PyAny>) -> Result { Ok(obj.extract::()? as f32) diff --git a/src/types/weakref/anyref.rs b/src/types/weakref/anyref.rs index 5b6ebda5717..9639fb96e3d 100644 --- a/src/types/weakref/anyref.rs +++ b/src/types/weakref/anyref.rs @@ -1,8 +1,12 @@ use crate::err::PyResult; use crate::ffi_ptr_ext::FfiPtrExt; +#[cfg(feature = "experimental-inspect")] +use crate::inspect::TypeHint; use crate::type_object::{PyTypeCheck, PyTypeInfo}; use crate::types::any::PyAny; use crate::{ffi, Bound}; +#[cfg(feature = "experimental-inspect")] +use crate::{type_hint, type_hint_union}; /// Represents any Python `weakref` reference. /// @@ -19,7 +23,11 @@ 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"; + const TYPE_HINT: TypeHint = type_hint_union!( + type_hint!("weakref", "ProxyType"), + type_hint!("weakref", "CallableProxyType"), + type_hint!("weakref", "ReferenceType") + ); 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 7bff0868184..6b456a26e07 100644 --- a/src/types/weakref/proxy.rs +++ b/src/types/weakref/proxy.rs @@ -1,11 +1,14 @@ +use super::PyWeakrefMethods; use crate::err::PyResult; use crate::ffi_ptr_ext::FfiPtrExt; +#[cfg(feature = "experimental-inspect")] +use crate::inspect::TypeHint; use crate::py_result_ext::PyResultExt; use crate::type_object::PyTypeCheck; use crate::types::any::PyAny; use crate::{ffi, Borrowed, Bound, BoundObject, IntoPyObject, IntoPyObjectExt}; - -use super::PyWeakrefMethods; +#[cfg(feature = "experimental-inspect")] +use crate::{type_hint, type_hint_union}; /// Represents any Python `weakref` Proxy type. /// @@ -23,7 +26,10 @@ 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"; + const TYPE_HINT: TypeHint = type_hint_union!( + type_hint!("weakref", "ProxyType"), + type_hint!("weakref", "CallableProxyType") + ); fn type_check(object: &Bound<'_, PyAny>) -> bool { unsafe { ffi::PyWeakref_CheckProxy(object.as_ptr()) > 0 } From 751482e7f1c90282c83c34891e8ea696cc84bda4 Mon Sep 17 00:00:00 2001 From: Thomas Pellissier-Tanon Date: Mon, 22 Sep 2025 15:28:54 +0200 Subject: [PATCH 2/2] Makes TypeHint an enum --- Cargo.toml | 1 - newsfragments/5438.changed.md | 3 +- noxfile.py | 14 +- pyo3-introspection/src/introspection.rs | 75 ++-- pyo3-introspection/src/model.rs | 27 +- pyo3-introspection/src/stubs.rs | 438 ++++++++++++++++++----- pyo3-macros-backend/src/frompyobject.rs | 16 +- pyo3-macros-backend/src/intopyobject.rs | 16 +- pyo3-macros-backend/src/introspection.rs | 27 +- pyo3-macros-backend/src/pyclass.rs | 4 +- pytests/Cargo.toml | 5 +- pytests/src/pyfunctions.rs | 6 +- src/conversion.rs | 6 +- src/conversions/std/num.rs | 24 +- src/conversions/std/string.rs | 12 +- src/impl_/extract_argument.rs | 9 +- src/inspect/mod.rs | 388 +++++++++++--------- src/type_object.rs | 4 +- src/types/boolobject.rs | 6 +- src/types/float.rs | 10 +- src/types/weakref/anyref.rs | 13 +- src/types/weakref/proxy.rs | 10 +- 22 files changed, 736 insertions(+), 378 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6ecdb9e50b4..29240e90b8a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -156,7 +156,6 @@ full = [ "chrono-tz", "either", "experimental-async", - "experimental-inspect", "eyre", "hashbrown", "indexmap", diff --git a/newsfragments/5438.changed.md b/newsfragments/5438.changed.md index dd713ae30c1..bcd7885673d 100644 --- a/newsfragments/5438.changed.md +++ b/newsfragments/5438.changed.md @@ -1 +1,2 @@ -Introspection: introduce `TypeHint` and make use of it to encode type hint annotations. \ No newline at end of file +Introspection: introduce `TypeHint` and make use of it to encode type hint annotations. +Rename `PyType{Info,Check}::TYPE_INFO` into `PyType{Info,Check}::TYPE_HINT` diff --git a/noxfile.py b/noxfile.py index 3cf76738135..474de382393 100644 --- a/noxfile.py +++ b/noxfile.py @@ -1094,7 +1094,15 @@ def test_introspection(session: nox.Session): for options in ([], ["--release"]): if target is not None: options += ("--target", target) - session.run_always("maturin", "develop", "-m", "./pytests/Cargo.toml", *options) + session.run_always( + "maturin", + "develop", + "-m", + "./pytests/Cargo.toml", + "--features", + "experimental-inspect", + *options, + ) # We look for the built library lib_file = None for file in Path(session.virtualenv.location).rglob("pyo3_pytests.*"): @@ -1154,6 +1162,10 @@ def _get_feature_sets() -> Tuple[Optional[str], ...]: # multiple-pymethods not supported on wasm features += ",multiple-pymethods" + if get_rust_version() >= (1, 83, 0): + # experimental-inspect requires 1.83+ + features += ",experimental-inspect" + if is_rust_nightly(): features += ",nightly" diff --git a/pyo3-introspection/src/introspection.rs b/pyo3-introspection/src/introspection.rs index b91d0ceb9d2..027c8d0e6b9 100644 --- a/pyo3-introspection/src/introspection.rs +++ b/pyo3-introspection/src/introspection.rs @@ -1,5 +1,5 @@ use crate::model::{ - Argument, Arguments, Attribute, Class, Function, Module, TypeHint, TypeHintImport, + Argument, Arguments, Attribute, Class, Function, Module, TypeHint, TypeHintExpr, VariableLengthArgument, }; use anyhow::{anyhow, bail, ensure, Context, Result}; @@ -10,12 +10,13 @@ use goblin::mach::symbols::{NO_SECT, N_SECT}; use goblin::mach::{Mach, MachO, SingleArch}; use goblin::pe::PE; use goblin::Object; +use serde::de::value::MapAccessDeserializer; use serde::de::{Error, MapAccess, Visitor}; -use serde::{de, Deserialize, Deserializer}; +use serde::{Deserialize, Deserializer}; use std::cmp::Ordering; use std::collections::HashMap; use std::path::Path; -use std::{fs, str, fmt}; +use std::{fmt, fs, str}; /// Introspect a cdylib built with PyO3 and returns the definition of a Python module. /// @@ -244,16 +245,26 @@ fn convert_attribute( } fn convert_type_hint(arg: &ChunkTypeHint) -> TypeHint { - TypeHint { - annotation: arg.annotation.clone(), - imports: arg.imports.iter().map(convert_type_hint_import).collect(), + match arg { + ChunkTypeHint::Ast(expr) => TypeHint::Ast(convert_type_hint_expr(expr)), + ChunkTypeHint::Plain(t) => TypeHint::Plain(t.clone()), } } -fn convert_type_hint_import(arg: &ChunkTypeHintImport) -> TypeHintImport { - TypeHintImport { - module: arg.module.clone(), - name: arg.name.clone(), +fn convert_type_hint_expr(expr: &ChunkTypeHintExpr) -> TypeHintExpr { + match expr { + ChunkTypeHintExpr::Builtin { id } => TypeHintExpr::Builtin { id: id.clone() }, + ChunkTypeHintExpr::Attribute { module, attr } => TypeHintExpr::Attribute { + module: module.clone(), + attr: attr.clone(), + }, + ChunkTypeHintExpr::Union { elts } => TypeHintExpr::Union { + elts: elts.iter().map(convert_type_hint_expr).collect(), + }, + ChunkTypeHintExpr::Subscript { value, slice } => TypeHintExpr::Subscript { + value: Box::new(convert_type_hint_expr(value)), + slice: slice.iter().map(convert_type_hint_expr).collect(), + }, } } @@ -408,7 +419,7 @@ enum Chunk { parent: Option, #[serde(default)] decorators: Vec, - #[serde(default, deserialize_with = "deserialize_annotation")] + #[serde(default, deserialize_with = "deserialize_type_hint")] returns: Option, }, Attribute { @@ -419,7 +430,7 @@ enum Chunk { name: String, #[serde(default)] value: Option, - #[serde(default, deserialize_with = "deserialize_annotation")] + #[serde(default, deserialize_with = "deserialize_type_hint")] annotation: Option, }, } @@ -443,24 +454,35 @@ struct ChunkArgument { name: String, #[serde(default)] default: Option, - #[serde(default, deserialize_with = "deserialize_annotation")] + #[serde(default, deserialize_with = "deserialize_type_hint")] annotation: Option, } -#[derive(Deserialize)] -struct ChunkTypeHint { - annotation: String, - #[serde(default)] - imports: Vec, +enum ChunkTypeHint { + Ast(ChunkTypeHintExpr), + Plain(String), } #[derive(Deserialize)] -struct ChunkTypeHintImport { - module: String, - name: String, +#[serde(tag = "type", rename_all = "lowercase")] +enum ChunkTypeHintExpr { + Builtin { + id: String, + }, + Attribute { + module: String, + attr: String, + }, + Union { + elts: Vec, + }, + Subscript { + value: Box, + slice: Vec, + }, } -fn deserialize_annotation<'de, D: Deserializer<'de>>( +fn deserialize_type_hint<'de, D: Deserializer<'de>>( deserializer: D, ) -> Result, D::Error> { struct AnnotationVisitor; @@ -483,14 +505,13 @@ fn deserialize_annotation<'de, D: Deserializer<'de>>( where E: Error, { - Ok(ChunkTypeHint { - annotation: v, - imports: Vec::new(), - }) + Ok(ChunkTypeHint::Plain(v)) } fn visit_map>(self, map: M) -> Result { - Deserialize::deserialize(de::value::MapAccessDeserializer::new(map)) + Ok(ChunkTypeHint::Ast(Deserialize::deserialize( + MapAccessDeserializer::new(map), + )?)) } } diff --git a/pyo3-introspection/src/model.rs b/pyo3-introspection/src/model.rs index dfcb42cea7d..5d13d15c51b 100644 --- a/pyo3-introspection/src/model.rs +++ b/pyo3-introspection/src/model.rs @@ -65,16 +65,27 @@ pub struct VariableLengthArgument { pub annotation: Option, } -/// A type hint annotation with the required modules to import +/// A type hint annotation +/// +/// Might be a plain string or an AST fragment #[derive(Debug, Eq, PartialEq, Clone, Hash)] -pub struct TypeHint { - pub annotation: String, - pub imports: Vec, +pub enum TypeHint { + Ast(TypeHintExpr), + Plain(String), } -/// An import required to make the type hint valid like `from {module} import {name}` +/// A type hint annotation as an AST fragment #[derive(Debug, Eq, PartialEq, Clone, Hash)] -pub struct TypeHintImport { - pub module: String, - pub name: String, +pub enum TypeHintExpr { + /// A Python builtin like `int` + Builtin { id: String }, + /// The attribute of a python object like `{value}.{attr}` + Attribute { module: String, attr: String }, + /// A union `{left} | {right}` + Union { elts: Vec }, + /// A subscript `{value}[*slice]` + Subscript { + value: Box, + slice: Vec, + }, } diff --git a/pyo3-introspection/src/stubs.rs b/pyo3-introspection/src/stubs.rs index e41470e8a39..b318e517b49 100644 --- a/pyo3-introspection/src/stubs.rs +++ b/pyo3-introspection/src/stubs.rs @@ -1,9 +1,10 @@ use crate::model::{ - Argument, Arguments, Attribute, Class, Function, Module, TypeHint, TypeHintImport, + Argument, Arguments, Attribute, Class, Function, Module, TypeHint, TypeHintExpr, VariableLengthArgument, }; use std::collections::{BTreeMap, BTreeSet, HashMap}; use std::path::PathBuf; +use std::str::FromStr; /// Generates the [type stubs](https://typing.readthedocs.io/en/latest/source/stubs.html) of a given module. /// It returns a map between the file name and the file content. @@ -44,16 +45,16 @@ fn add_module_stub_files( /// Generates the module stubs to a String, not including submodules fn module_stubs(module: &Module, parents: &[&str]) -> String { - let mut imports = Imports::new(); + let imports = Imports::create(module, parents); let mut elements = Vec::new(); for attribute in &module.attributes { - elements.push(attribute_stubs(attribute, &mut imports)); + elements.push(attribute_stubs(attribute, &imports)); } for class in &module.classes { - elements.push(class_stubs(class, &mut imports)); + elements.push(class_stubs(class, &imports)); } for function in &module.functions { - elements.push(function_stubs(function, &mut imports)); + elements.push(function_stubs(function, &imports)); } // We generate a __getattr__ method to tag incomplete stubs @@ -68,31 +69,22 @@ fn module_stubs(module: &Module, parents: &[&str]) -> String { arguments: vec![Argument { name: "name".to_string(), default_value: None, - annotation: Some(TypeHint { - annotation: "str".into(), - imports: Vec::new(), - }), + annotation: Some(TypeHint::Ast(TypeHintExpr::Builtin { id: "str".into() })), }], vararg: None, keyword_only_arguments: Vec::new(), kwarg: None, }, - returns: Some(TypeHint { - annotation: "Incomplete".into(), - imports: vec![TypeHintImport { - module: "_typeshed".into(), - name: "Incomplete".into(), - }], - }), + returns: Some(TypeHint::Ast(TypeHintExpr::Attribute { + module: "_typeshed".into(), + attr: "Incomplete".into(), + })), }, - &mut imports, + &imports, )); } - // We validate the imports - imports.filter_for_module(&module.name, parents); - - let mut final_elements = imports.to_lines(); + let mut final_elements = imports.imports; final_elements.extend(elements); let mut output = String::new(); @@ -117,7 +109,7 @@ fn module_stubs(module: &Module, parents: &[&str]) -> String { output } -fn class_stubs(class: &Class, imports: &mut Imports) -> String { +fn class_stubs(class: &Class, imports: &Imports) -> String { let mut buffer = format!("class {}:", class.name); if class.methods.is_empty() && class.attributes.is_empty() { buffer.push_str(" ..."); @@ -136,7 +128,7 @@ fn class_stubs(class: &Class, imports: &mut Imports) -> String { buffer } -fn function_stubs(function: &Function, imports: &mut Imports) -> String { +fn function_stubs(function: &Function, imports: &Imports) -> String { // Signature let mut parameters = Vec::new(); for argument in &function.arguments.positional_only_arguments { @@ -178,107 +170,303 @@ fn function_stubs(function: &Function, imports: &mut Imports) -> String { buffer.push(')'); if let Some(returns) = &function.returns { buffer.push_str(" -> "); - buffer.push_str(type_hint_stub(returns, imports)); + type_hint_stub(returns, imports, &mut buffer); } buffer.push_str(": ..."); buffer } -fn attribute_stubs(attribute: &Attribute, imports: &mut Imports) -> String { - let mut output = attribute.name.clone(); +fn attribute_stubs(attribute: &Attribute, imports: &Imports) -> String { + let mut buffer = attribute.name.clone(); if let Some(annotation) = &attribute.annotation { - output.push_str(": "); - output.push_str(type_hint_stub(annotation, imports)); + buffer.push_str(": "); + type_hint_stub(annotation, imports, &mut buffer); } if let Some(value) = &attribute.value { - output.push_str(" = "); - output.push_str(value); + buffer.push_str(" = "); + buffer.push_str(value); } - output + buffer } -fn argument_stub(argument: &Argument, imports: &mut Imports) -> String { - let mut output = argument.name.clone(); +fn argument_stub(argument: &Argument, imports: &Imports) -> String { + let mut buffer = argument.name.clone(); if let Some(annotation) = &argument.annotation { - output.push_str(": "); - output.push_str(type_hint_stub(annotation, imports)); + buffer.push_str(": "); + type_hint_stub(annotation, imports, &mut buffer); } if let Some(default_value) = &argument.default_value { - output.push_str(if argument.annotation.is_some() { + buffer.push_str(if argument.annotation.is_some() { " = " } else { "=" }); - output.push_str(default_value); + buffer.push_str(default_value); } - output + buffer } -fn variable_length_argument_stub( - argument: &VariableLengthArgument, - imports: &mut Imports, -) -> String { - let mut output = argument.name.clone(); +fn variable_length_argument_stub(argument: &VariableLengthArgument, imports: &Imports) -> String { + let mut buffer = argument.name.clone(); if let Some(annotation) = &argument.annotation { - output.push_str(": "); - output.push_str(type_hint_stub(annotation, imports)); + buffer.push_str(": "); + type_hint_stub(annotation, imports, &mut buffer); } - output + buffer } -fn type_hint_stub<'a>(annotation: &'a TypeHint, imports: &mut Imports) -> &'a str { - for import in &annotation.imports { - imports.add(import); +fn type_hint_stub(type_hint: &TypeHint, imports: &Imports, buffer: &mut String) { + match type_hint { + TypeHint::Ast(t) => imports.serialize_type_hint(t, buffer), + TypeHint::Plain(t) => buffer.push_str(t), } - &annotation.annotation } /// Datastructure to deduplicate, validate and generate imports +#[derive(Default)] struct Imports { - /// module -> names - imports: BTreeMap>, + /// Import lines ready to use + imports: Vec, + /// Renaming map: from module name and member name return the name to use in type hints + renaming: BTreeMap<(String, String), String>, } impl Imports { + /// This generates a map from the builtin or module name to the actual alias used in the file + /// + /// For Python builtins and elements declared by the module the alias is always the actual name. + /// + /// For other elements, we can alias them using the `from X import Y as Z` syntax. + /// So, we first list all builtins and local elements, then iterate on imports + /// and create the aliases when needed. + fn create(module: &Module, module_parents: &[&str]) -> Self { + let mut elements_used_in_annotations = ElementsUsedInAnnotations::new(); + elements_used_in_annotations.walk_module(module); + + let mut imports = Vec::new(); + let mut renaming = BTreeMap::new(); + let mut local_name_to_module_and_attribute = BTreeMap::new(); + + // We first process local and built-ins elements, they are never aliased or imported + for name in module + .classes + .iter() + .map(|c| c.name.clone()) + .chain(module.functions.iter().map(|f| f.name.clone())) + .chain(module.attributes.iter().map(|a| a.name.clone())) + .chain(elements_used_in_annotations.builtins) + { + local_name_to_module_and_attribute.insert(name.clone(), (None, name.clone())); + } + + // We compute the set of ways the current module can be named + let mut possible_current_module_names = vec![module.name.clone()]; + let mut current_module_name = Some(module.name.clone()); + for parent in module_parents.iter().rev() { + let path = if let Some(current) = current_module_name { + format!("{parent}.{current}") + } else { + parent.to_string() + }; + possible_current_module_names.push(path.clone()); + current_module_name = Some(path); + } + + // We process then imports, normalizing local imports + for (module, attrs) in elements_used_in_annotations.module_members { + let normalized_module = if possible_current_module_names.contains(&module) { + None + } else { + Some(module.clone()) + }; + let mut import_for_module = Vec::new(); + for attr in attrs { + let mut local_name = attr.clone(); + while let Some((possible_conflict_module, possible_conflict_attr)) = + local_name_to_module_and_attribute.get(&local_name) + { + if *possible_conflict_module == normalized_module + && *possible_conflict_attr == attr + { + break; // It's the same + } + // We generate a new local name + let number_of_digits_at_the_end = local_name + .bytes() + .rev() + .take_while(|b| b.is_ascii_digit()) + .count(); + let (local_name_prefix, local_name_number) = + local_name.split_at(local_name.len() - number_of_digits_at_the_end); + local_name = format!( + "{local_name_prefix}{}", + u64::from_str(local_name_number).unwrap_or(1) + 1 + ); + } + local_name_to_module_and_attribute.insert( + local_name.clone(), + (normalized_module.clone(), attr.clone()), + ); + renaming.insert((module.clone(), attr.clone()), local_name.clone()); + import_for_module.push(if local_name == attr { + attr + } else { + format!("{attr} as {local_name}") + }); + } + if let Some(module) = normalized_module { + imports.push(format!( + "from {module} import {}", + import_for_module.join(", ") + )); + } + } + + Self { imports, renaming } + } + + fn serialize_type_hint(&self, expr: &TypeHintExpr, buffer: &mut String) { + match expr { + TypeHintExpr::Builtin { id } => buffer.push_str(id), + TypeHintExpr::Attribute { module, attr } => { + let alias = self + .renaming + .get(&(module.clone(), attr.clone())) + .expect("All type hint attributes should have been visited"); + buffer.push_str(alias) + } + TypeHintExpr::Union { elts } => { + for (i, elt) in elts.iter().enumerate() { + if i > 0 { + buffer.push_str(" | "); + } + self.serialize_type_hint(elt, buffer); + } + } + TypeHintExpr::Subscript { value, slice } => { + self.serialize_type_hint(value, buffer); + buffer.push('['); + for (i, elt) in slice.iter().enumerate() { + if i > 0 { + buffer.push_str(", "); + } + self.serialize_type_hint(elt, buffer); + } + buffer.push(']'); + } + } + } +} + +/// Lists all the elements used in annotations +struct ElementsUsedInAnnotations { + /// module -> name + module_members: BTreeMap>, + builtins: BTreeSet, +} + +impl ElementsUsedInAnnotations { fn new() -> Self { Self { - imports: BTreeMap::new(), + module_members: BTreeMap::new(), + builtins: BTreeSet::new(), } } - fn add(&mut self, import: &TypeHintImport) { - self.imports - .entry(import.module.clone()) - .or_default() - .insert(import.name.clone()); + fn walk_module(&mut self, module: &Module) { + for attr in &module.attributes { + self.walk_attribute(attr); + } + for class in &module.classes { + self.walk_class(class); + } + for function in &module.functions { + self.walk_function(function); + } + if module.incomplete { + self.builtins.insert("str".into()); + self.module_members + .entry("_typeshed".into()) + .or_default() + .insert("Incomplete".into()); + } } - /// Remove all local import paths i.e. 'foo' and 'bar.foo' if the module is 'bar.foo' (encoded as name = 'foo' and parents = \['bar'\] - fn filter_for_module(&mut self, name: &str, parents: &[&str]) { - let mut local_import_path = name.to_string(); - self.imports.remove(name); - for parent in parents { - local_import_path = format!("{local_import_path}.{parent}"); - self.imports.remove(&local_import_path); + fn walk_class(&mut self, class: &Class) { + for method in &class.methods { + self.walk_function(method); + } + for attr in &class.attributes { + self.walk_attribute(attr); } } - fn to_lines(&self) -> Vec { - let mut lines = Vec::with_capacity(self.imports.len()); - for (module, names) in &self.imports { - let mut output = String::new(); - output.push_str("from "); - output.push_str(module); - output.push_str(" import "); - for (i, name) in names.iter().enumerate() { - if i > 0 { - output.push_str(", "); + fn walk_attribute(&mut self, attribute: &Attribute) { + if let Some(type_hint) = &attribute.annotation { + self.walk_type_hint(type_hint); + } + } + + fn walk_function(&mut self, function: &Function) { + for decorator in &function.decorators { + self.builtins.insert(decorator.clone()); + } + for arg in function + .arguments + .positional_only_arguments + .iter() + .chain(&function.arguments.arguments) + .chain(&function.arguments.keyword_only_arguments) + { + if let Some(type_hint) = &arg.annotation { + self.walk_type_hint(type_hint); + } + } + for arg in function + .arguments + .vararg + .as_ref() + .iter() + .chain(&function.arguments.kwarg.as_ref()) + { + if let Some(type_hint) = &arg.annotation { + self.walk_type_hint(type_hint); + } + } + if let Some(type_hint) = &function.returns { + self.walk_type_hint(type_hint); + } + } + + fn walk_type_hint(&mut self, type_hint: &TypeHint) { + if let TypeHint::Ast(type_hint) = type_hint { + self.walk_type_hint_expr(type_hint); + } + } + + fn walk_type_hint_expr(&mut self, expr: &TypeHintExpr) { + match expr { + TypeHintExpr::Builtin { id } => { + self.builtins.insert(id.clone()); + } + TypeHintExpr::Attribute { module, attr } => { + self.module_members + .entry(module.clone()) + .or_default() + .insert(attr.clone()); + } + TypeHintExpr::Union { elts } => { + for elt in elts { + self.walk_type_hint_expr(elt) + } + } + TypeHintExpr::Subscript { value, slice } => { + self.walk_type_hint_expr(value); + for elt in slice { + self.walk_type_hint_expr(elt); } - output.push_str(name); } - lines.push(output); } - lines } } @@ -310,27 +498,18 @@ mod tests { keyword_only_arguments: vec![Argument { name: "karg".into(), default_value: None, - annotation: Some(TypeHint { - annotation: "str".into(), - imports: Vec::new(), - }), + annotation: Some(TypeHint::Plain("str".into())), }], kwarg: Some(VariableLengthArgument { name: "kwarg".into(), - annotation: Some(TypeHint { - annotation: "str".into(), - imports: Vec::new(), - }), + annotation: Some(TypeHint::Plain("str".into())), }), }, - returns: Some(TypeHint { - annotation: "list[str]".into(), - imports: Vec::new(), - }), + returns: Some(TypeHint::Plain("list[str]".into())), }; assert_eq!( "def func(posonly, /, arg, *varargs, karg: str, **kwarg: str) -> list[str]: ...", - function_stubs(&function, &mut Imports::new()) + function_stubs(&function, &Imports::default()) ) } @@ -354,10 +533,7 @@ mod tests { keyword_only_arguments: vec![Argument { name: "karg".into(), default_value: Some("\"foo\"".into()), - annotation: Some(TypeHint { - annotation: "str".into(), - imports: Vec::new(), - }), + annotation: Some(TypeHint::Plain("str".into())), }], kwarg: None, }, @@ -365,7 +541,77 @@ mod tests { }; assert_eq!( "def afunc(posonly=1, /, arg=True, *, karg: str = \"foo\"): ...", - function_stubs(&function, &mut Imports::new()) + function_stubs(&function, &Imports::default()) ) } + + #[test] + fn test_import() { + let big_type = TypeHintExpr::Subscript { + value: Box::new(TypeHintExpr::Builtin { id: "dict".into() }), + slice: vec![ + TypeHintExpr::Attribute { + module: "foo.bar".into(), + attr: "A".into(), + }, + TypeHintExpr::Union { + elts: vec![ + TypeHintExpr::Attribute { + module: "bar".into(), + attr: "A".into(), + }, + TypeHintExpr::Attribute { + module: "foo".into(), + attr: "A".into(), + }, + TypeHintExpr::Attribute { + module: "foo".into(), + attr: "B".into(), + }, + TypeHintExpr::Attribute { + module: "bat".into(), + attr: "A".into(), + }, + ], + }, + ], + }; + let imports = Imports::create( + &Module { + name: "bar".into(), + modules: Vec::new(), + classes: vec![Class { + name: "A".into(), + methods: Vec::new(), + attributes: Vec::new(), + }], + functions: vec![Function { + name: String::new(), + decorators: Vec::new(), + arguments: Arguments { + positional_only_arguments: Vec::new(), + arguments: Vec::new(), + vararg: None, + keyword_only_arguments: Vec::new(), + kwarg: None, + }, + returns: Some(TypeHint::Ast(big_type.clone())), + }], + attributes: Vec::new(), + incomplete: true, + }, + &["foo"], + ); + assert_eq!( + &imports.imports, + &[ + "from _typeshed import Incomplete", + "from bat import A as A2", + "from foo import A as A3, B" + ] + ); + let mut output = String::new(); + imports.serialize_type_hint(&big_type, &mut output); + assert_eq!(output, "dict[A, A | A3 | B | A2]"); + } } diff --git a/pyo3-macros-backend/src/frompyobject.rs b/pyo3-macros-backend/src/frompyobject.rs index 79b8a75d1fb..fd6e5666cbf 100644 --- a/pyo3-macros-backend/src/frompyobject.rs +++ b/pyo3-macros-backend/src/frompyobject.rs @@ -102,12 +102,14 @@ impl<'a> Enum<'a> { #[cfg(feature = "experimental-inspect")] fn input_type(&self, ctx: &Ctx) -> TokenStream { let pyo3_crate_path = &ctx.pyo3_path; - let union = self + let variants = self .variants .iter() .map(|var| var.input_type(ctx)) .collect::>(); - quote! { #pyo3_crate_path::type_hint_union!(#(#union),*) } + quote! { + #pyo3_crate_path::inspect::TypeHint::union(&[#(#variants),*]) + } } } @@ -475,11 +477,11 @@ impl<'a> Container<'a> { Self::field_input_type(from_py_with, ty, ctx) }) .collect::>(); - quote! { #pyo3_crate_path::type_hint_subscript!(#pyo3_crate_path::type_hint!("tuple")#(, #elements)*) } + quote! { #pyo3_crate_path::inspect::TypeHint::subscript(&#pyo3_crate_path::inspect::TypeHint::builtin("tuple"), &[#(#elements),*]) } } ContainerType::Struct(_) => { // TODO: implement using a Protocol? - quote! { #pyo3_crate_path::type_hint!("_typeshed", "Incomplete") } + quote! { #pyo3_crate_path::inspect::TypeHint::module_member("_typeshed", "Incomplete") } } } } @@ -493,7 +495,7 @@ impl<'a> Container<'a> { let pyo3_crate_path = &ctx.pyo3_path; if from_py_with.is_some() { // We don't know what from_py_with is doing - quote! { #pyo3_crate_path::type_hint!("_typeshed", "Incomplete") } + quote! { #pyo3_crate_path::inspect::TypeHint::module_member("_typeshed", "Incomplete") } } else { let mut ty = ty.clone(); elide_lifetimes(&mut ty); @@ -583,13 +585,13 @@ pub fn build_derive_from_pyobject(tokens: &DeriveInput) -> Result { } syn::Data::Union(_) => { // Not supported at this point - quote! { #pyo3_crate_path::type_hint!("_typeshed", "Incomplete") } + quote! { #pyo3_crate_path::inspect::TypeHint::module_member("_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::type_hint!("_typeshed", "Incomplete") } + quote! { #pyo3_crate_path::inspect::TypeHint::module_member("_typeshed", "Incomplete") } }; quote! { const INPUT_TYPE: #pyo3_crate_path::inspect::TypeHint = #input_type; } }; diff --git a/pyo3-macros-backend/src/intopyobject.rs b/pyo3-macros-backend/src/intopyobject.rs index b7bbb928510..737a79d6e92 100644 --- a/pyo3-macros-backend/src/intopyobject.rs +++ b/pyo3-macros-backend/src/intopyobject.rs @@ -378,11 +378,11 @@ impl<'a, const REF: bool> Container<'a, REF> { }, ) .collect::>(); - quote! { #pyo3_crate_path::type_hint_subscript!(#pyo3_crate_path::type_hint!("tuple")#(, #elements)*) } + quote! { #pyo3_crate_path::inspect::TypeHint::subscript(&#pyo3_crate_path::inspect::TypeHint::builtin("tuple"), &[#(#elements),*]) } } ContainerType::Struct(_) => { // TODO: implement using a Protocol? - quote! { #pyo3_crate_path::type_hint!("_typeshed", "Incomplete") } + quote! { #pyo3_crate_path::inspect::TypeHint::module_member("_typeshed", "Incomplete") } } } } @@ -396,7 +396,7 @@ impl<'a, const REF: bool> Container<'a, REF> { let pyo3_crate_path = &ctx.pyo3_path; if into_py_with.is_some() { // We don't know what into_py_with is doing - quote! { #pyo3_crate_path::type_hint!("_typeshed", "Incomplete") } + quote! { #pyo3_crate_path::inspect::TypeHint::module_member("_typeshed", "Incomplete") } } else { let mut ty = ty.clone(); elide_lifetimes(&mut ty); @@ -482,12 +482,14 @@ 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 union = self + let variants = self .variants .iter() .map(|var| var.output_type(ctx)) .collect::>(); - quote! { #pyo3_crate_path::type_hint_union!(#(#union),*) } + quote! { + #pyo3_crate_path::inspect::TypeHint::union(&[#(#variants),*]) + } } } @@ -604,13 +606,13 @@ pub fn build_derive_into_pyobject(tokens: &DeriveInput) -> Resu } syn::Data::Union(_) => { // Not supported at this point - quote! { #pyo3_crate_path::type_hint!("_typeshed", "Incomplete") } + quote! { #pyo3_crate_path::inspect::TypeHint::module_member("_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::type_hint!("_typeshed", "Incomplete") } + quote! { #pyo3_crate_path::inspect::TypeHint::module_member("_typeshed", "Incomplete") } }; quote! { const OUTPUT_TYPE: #pyo3_path::inspect::TypeHint = #output_type; } }; diff --git a/pyo3-macros-backend/src/introspection.rs b/pyo3-macros-backend/src/introspection.rs index 6c9382f9533..7b77e24c676 100644 --- a/pyo3-macros-backend/src/introspection.rs +++ b/pyo3-macros-backend/src/introspection.rs @@ -411,9 +411,9 @@ impl IntrospectionNode<'_> { >>::INPUT_TYPE }; if nullable { - annotation = quote! { #pyo3_crate_path::type_hint_union!(#annotation, #pyo3_crate_path::type_hint!("None")) }; + annotation = quote! { #pyo3_crate_path::inspect::TypeHint::union(&[#annotation, #pyo3_crate_path::inspect::TypeHint::builtin("None")]) }; } - content.push_tokens(quote! { #pyo3_crate_path::type_hint_json!(#annotation) }); + content.push_tokens(serialize_type_hint(annotation, pyo3_crate_path)); } Self::OutputType { rust_type, @@ -421,17 +421,17 @@ impl IntrospectionNode<'_> { } => { let mut annotation = quote! { <#rust_type as #pyo3_crate_path::impl_::introspection::PyReturnType>::OUTPUT_TYPE }; if is_final { - annotation = quote! { #pyo3_crate_path::type_hint_subscript!(#pyo3_crate_path::type_hint!("typing", "Final"), #annotation) }; + annotation = quote! { #pyo3_crate_path::inspect::TypeHint::subscript(&#pyo3_crate_path::inspect::TypeHint::module_member("typing", "Final"), &[#annotation]) }; } - content.push_tokens(quote! { #pyo3_crate_path::type_hint_json!(#annotation) }); + content.push_tokens(serialize_type_hint(annotation, pyo3_crate_path)); } Self::ConstantType { name, module } => { let annotation = if let Some(module) = module { - quote! { #pyo3_crate_path::type_hint!(#module, #name) } + quote! { #pyo3_crate_path::inspect::TypeHint::module_member(#module, #name) } } else { - quote! { #pyo3_crate_path::type_hint!(#name) } + quote! { #pyo3_crate_path::inspect::TypeHint::builtin(#name) } }; - content.push_tokens(quote! { #pyo3_crate_path::type_hint_json!(#annotation) }); + content.push_tokens(serialize_type_hint(annotation, pyo3_crate_path)); } Self::Map(map) => { content.push_str("{"); @@ -472,6 +472,19 @@ impl IntrospectionNode<'_> { } } +fn serialize_type_hint(hint: TokenStream, pyo3_crate_path: &PyO3CratePath) -> TokenStream { + quote! {{ + const TYPE_HINT: #pyo3_crate_path::inspect::TypeHint = #hint; + const TYPE_HINT_LEN: usize = TYPE_HINT.serialized_len_for_introspection(); + const TYPE_HINT_SER: [u8; TYPE_HINT_LEN] = { + let mut result: [u8; TYPE_HINT_LEN] = [0; TYPE_HINT_LEN]; + TYPE_HINT.serialize_for_introspection(&mut result); + result + }; + &TYPE_HINT_SER + }} +} + struct AttributedIntrospectionNode<'a> { node: IntrospectionNode<'a>, attributes: &'a [Attribute], diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index e26eece1e89..30ee83abb0c 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -419,9 +419,9 @@ fn get_class_type_hint(cls: &Ident, args: &PyClassArgs, ctx: &Ctx) -> TokenStrea let name = get_class_python_name(cls, args).to_string(); if let Some(module) = &args.options.module { let module = module.value.value(); - quote! { #pyo3_path::type_hint!(#module, #name) } + quote! { #pyo3_path::inspect::TypeHint::module_member(#module, #name) } } else { - quote! { #pyo3_path::type_hint!(#name) } + quote! { #pyo3_path::inspect::TypeHint::builtin(#name) } } } diff --git a/pytests/Cargo.toml b/pytests/Cargo.toml index 0b8dfe5d5ba..2e3800d2e15 100644 --- a/pytests/Cargo.toml +++ b/pytests/Cargo.toml @@ -7,8 +7,11 @@ edition = "2021" publish = false rust-version = "1.74" +[features] +experimental-inspect = ["pyo3/experimental-inspect"] + [dependencies] -pyo3 = { path = "../", features = ["experimental-inspect"] } +pyo3.path = "../" [build-dependencies] pyo3-build-config = { path = "../pyo3-build-config" } diff --git a/pytests/src/pyfunctions.rs b/pytests/src/pyfunctions.rs index ce7494f7c5f..5cb88f976ab 100644 --- a/pytests/src/pyfunctions.rs +++ b/pytests/src/pyfunctions.rs @@ -77,6 +77,7 @@ fn with_typed_args(a: bool, b: u64, c: f64, d: &str) -> (bool, u64, f64, &str) { (a, b, c, d) } +#[cfg(feature = "experimental-inspect")] #[pyfunction(signature = (a: "int", *_args: "str", _b: "int | None" = None, **_kwargs: "bool") -> "int")] fn with_custom_type_annotations<'py>( a: Any<'py>, @@ -135,9 +136,12 @@ fn many_keyword_arguments<'py>( #[pymodule] pub mod pyfunctions { + #[cfg(feature = "experimental-inspect")] + #[pymodule_export] + use super::with_custom_type_annotations; #[pymodule_export] use super::{ args_kwargs, many_keyword_arguments, none, positional_only, simple, simple_args, - simple_args_kwargs, simple_kwargs, with_custom_type_annotations, with_typed_args, + simple_args_kwargs, simple_kwargs, with_typed_args, }; } diff --git a/src/conversion.rs b/src/conversion.rs index 4035484b761..009114df5ff 100644 --- a/src/conversion.rs +++ b/src/conversion.rs @@ -6,8 +6,6 @@ use crate::inspect::types::TypeInfo; use crate::inspect::TypeHint; use crate::pyclass::boolean_struct::False; use crate::pyclass::{PyClassGuardError, PyClassGuardMutError}; -#[cfg(feature = "experimental-inspect")] -use crate::type_hint; use crate::types::PyTuple; use crate::{ Borrowed, Bound, BoundObject, Py, PyAny, PyClass, PyClassGuard, PyErr, PyRef, PyRefMut, Python, @@ -63,7 +61,7 @@ pub trait IntoPyObject<'py>: Sized { /// For most types, the return value for this method will be identical to that of [`FromPyObject::INPUT_TYPE`]. /// It may be different for some types, such as `Dict`, to allow duck-typing: functions return `Dict` but take `Mapping` as argument. #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: TypeHint = type_hint!("typing", "Any"); + const OUTPUT_TYPE: TypeHint = TypeHint::module_member("typing", "Any"); /// Performs the conversion. fn into_pyobject(self, py: Python<'py>) -> Result; @@ -394,7 +392,7 @@ pub trait FromPyObject<'a, 'py>: Sized { /// 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: TypeHint = type_hint!("typing", "Any"); + const INPUT_TYPE: TypeHint = TypeHint::module_member("typing", "Any"); /// Extracts `Self` from the bound smart pointer `obj`. /// diff --git a/src/conversions/std/num.rs b/src/conversions/std/num.rs index 881f6bce2a6..e08b989c00f 100644 --- a/src/conversions/std/num.rs +++ b/src/conversions/std/num.rs @@ -5,8 +5,6 @@ use crate::ffi_ptr_ext::FfiPtrExt; use crate::inspect::types::TypeInfo; #[cfg(feature = "experimental-inspect")] use crate::inspect::TypeHint; -#[cfg(feature = "experimental-inspect")] -use crate::type_hint; use crate::types::{PyByteArray, PyByteArrayMethods, PyBytes, PyInt}; use crate::{exceptions, ffi, Borrowed, Bound, FromPyObject, PyAny, PyErr, PyResult, Python}; use std::convert::Infallible; @@ -110,7 +108,7 @@ macro_rules! int_convert_u64_or_i64 { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: TypeHint = type_hint!("int"); + const OUTPUT_TYPE: TypeHint = TypeHint::builtin("int"); fn into_pyobject(self, py: Python<'py>) -> Result { unsafe { @@ -142,7 +140,7 @@ macro_rules! int_convert_u64_or_i64 { type Error = PyErr; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: TypeHint = type_hint!("int"); + const INPUT_TYPE: TypeHint = TypeHint::builtin("int"); fn extract(obj: Borrowed<'_, '_, PyAny>) -> Result<$rust_type, Self::Error> { extract_int!(obj, !0, $pylong_as_ll_or_ull, $force_index_call) @@ -164,7 +162,7 @@ macro_rules! int_fits_c_long { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: TypeHint = type_hint!("int"); + const OUTPUT_TYPE: TypeHint = TypeHint::builtin("int"); fn into_pyobject(self, py: Python<'py>) -> Result { unsafe { @@ -203,7 +201,7 @@ macro_rules! int_fits_c_long { type Error = PyErr; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: TypeHint = type_hint!("int"); + const INPUT_TYPE: TypeHint = TypeHint::builtin("int"); fn extract(obj: Borrowed<'_, 'py, PyAny>) -> Result { let val: c_long = extract_int!(obj, -1, ffi::PyLong_AsLong)?; @@ -225,7 +223,7 @@ impl<'py> IntoPyObject<'py> for u8 { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: TypeHint = type_hint!("int"); + const OUTPUT_TYPE: TypeHint = TypeHint::builtin("int"); fn into_pyobject(self, py: Python<'py>) -> Result { unsafe { @@ -288,7 +286,7 @@ impl<'py> FromPyObject<'_, 'py> for u8 { type Error = PyErr; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: TypeHint = type_hint!("int"); + const INPUT_TYPE: TypeHint = TypeHint::builtin("int"); fn extract(obj: Borrowed<'_, 'py, PyAny>) -> Result { let val: c_long = extract_int!(obj, -1, ffi::PyLong_AsLong)?; @@ -436,7 +434,7 @@ mod fast_128bit_int_conversion { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: TypeHint = type_hint!("int"); + const OUTPUT_TYPE: TypeHint = TypeHint::builtin("int"); fn into_pyobject(self, py: Python<'py>) -> Result { #[cfg(not(Py_3_13))] @@ -510,7 +508,7 @@ mod fast_128bit_int_conversion { type Error = PyErr; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: TypeHint = type_hint!("int"); + const INPUT_TYPE: TypeHint = TypeHint::builtin("int"); fn extract(ob: Borrowed<'_, '_, PyAny>) -> Result<$rust_type, Self::Error> { let num = @@ -586,7 +584,7 @@ mod slow_128bit_int_conversion { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: TypeHint = type_hint!("int"); + const OUTPUT_TYPE: TypeHint = TypeHint::builtin("int"); fn into_pyobject(self, py: Python<'py>) -> Result { let lower = (self as u64).into_pyobject(py)?; @@ -631,7 +629,7 @@ mod slow_128bit_int_conversion { type Error = PyErr; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: TypeHint = type_hint!("int"); + const INPUT_TYPE: TypeHint = TypeHint::builtin("int"); fn extract(ob: Borrowed<'_, '_, PyAny>) -> Result<$rust_type, Self::Error> { let py = ob.py(); @@ -685,7 +683,7 @@ macro_rules! nonzero_int_impl { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: TypeHint = type_hint!("int"); + const OUTPUT_TYPE: TypeHint = TypeHint::builtin("int"); #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { diff --git a/src/conversions/std/string.rs b/src/conversions/std/string.rs index 92717f13950..f46c159967d 100644 --- a/src/conversions/std/string.rs +++ b/src/conversions/std/string.rs @@ -2,8 +2,6 @@ use crate::inspect::types::TypeInfo; #[cfg(feature = "experimental-inspect")] use crate::inspect::TypeHint; -#[cfg(feature = "experimental-inspect")] -use crate::type_hint; use crate::{ conversion::IntoPyObject, instance::Bound, types::PyString, Borrowed, FromPyObject, PyAny, PyErr, Python, @@ -130,7 +128,7 @@ impl<'py> IntoPyObject<'py> for String { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: TypeHint = type_hint!("str"); + const OUTPUT_TYPE: TypeHint = TypeHint::builtin("str"); fn into_pyobject(self, py: Python<'py>) -> Result { Ok(PyString::new(py, &self)) @@ -166,7 +164,7 @@ impl<'a> crate::conversion::FromPyObject<'a, '_> for &'a str { type Error = PyErr; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: TypeHint = type_hint!("str"); + const INPUT_TYPE: TypeHint = TypeHint::builtin("str"); fn extract(ob: crate::Borrowed<'a, '_, PyAny>) -> Result { ob.cast::()?.to_str() @@ -182,7 +180,7 @@ impl<'a> crate::conversion::FromPyObject<'a, '_> for Cow<'a, str> { type Error = PyErr; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: TypeHint = type_hint!("str"); + const INPUT_TYPE: TypeHint = TypeHint::builtin("str"); fn extract(ob: crate::Borrowed<'a, '_, PyAny>) -> Result { ob.cast::()?.to_cow() @@ -200,7 +198,7 @@ impl FromPyObject<'_, '_> for String { type Error = PyErr; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: TypeHint = type_hint!("str"); + const INPUT_TYPE: TypeHint = TypeHint::builtin("str"); fn extract(obj: Borrowed<'_, '_, PyAny>) -> Result { obj.cast::()?.to_cow().map(Cow::into_owned) @@ -216,7 +214,7 @@ impl FromPyObject<'_, '_> for char { type Error = PyErr; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: TypeHint = type_hint!("str"); + const INPUT_TYPE: TypeHint = TypeHint::builtin("str"); fn extract(obj: Borrowed<'_, '_, PyAny>) -> Result { let s = obj.cast::()?.to_cow()?; diff --git a/src/impl_/extract_argument.rs b/src/impl_/extract_argument.rs index 86ce118539f..67be55e46ab 100644 --- a/src/impl_/extract_argument.rs +++ b/src/impl_/extract_argument.rs @@ -8,8 +8,6 @@ use crate::{ Borrowed, Bound, DowncastError, FromPyObject, PyAny, PyClass, PyClassGuard, PyClassGuardMut, PyErr, PyResult, PyTypeCheck, Python, }; -#[cfg(feature = "experimental-inspect")] -use crate::{type_hint, type_hint_union}; /// Helper type used to keep implementation more concise. /// @@ -113,7 +111,10 @@ where type Error = T::Error; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: TypeHint = type_hint_union!(type_hint!("typing", "Any"), type_hint!("None")); + const INPUT_TYPE: TypeHint = TypeHint::union(&[ + TypeHint::module_member("typing", "Any"), + TypeHint::builtin("None"), + ]); #[inline] fn extract( @@ -134,7 +135,7 @@ impl<'a, 'holder, 'py> PyFunctionArgument<'a, 'holder, 'py, false> for &'holder type Error = as FromPyObject<'a, 'py>>::Error; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: TypeHint = type_hint!("str"); + const INPUT_TYPE: TypeHint = TypeHint::builtin("str"); #[inline] fn extract( diff --git a/src/inspect/mod.rs b/src/inspect/mod.rs index feb89ab6448..43906ff0eae 100644 --- a/src/inspect/mod.rs +++ b/src/inspect/mod.rs @@ -7,185 +7,233 @@ use std::fmt::Formatter; pub mod types; -/// A [type hint](https://docs.python.org/3/glossary.html#term-type-hint) with a list of imports to make it valid +/// A [type hint](https://docs.python.org/3/glossary.html#term-type-hint). /// /// This struct aims at being used in `const` contexts like in [`FromPyObject::INPUT_TYPE`](crate::FromPyObject::INPUT_TYPE) and [`IntoPyObject::OUTPUT_TYPE`](crate::IntoPyObject::OUTPUT_TYPE). /// /// ``` -/// use pyo3::{type_hint, type_hint_union}; /// use pyo3::inspect::TypeHint; /// -/// const T: TypeHint = type_hint_union!(type_hint!("int"), type_hint!("b", "B")); -/// assert_eq!(T.to_string(), "int | B"); +/// const T: TypeHint = TypeHint::union(&[TypeHint::builtin("int"), TypeHint::module_member("b", "B")]); +/// assert_eq!(T.to_string(), "int | b.B"); /// ``` #[derive(Clone, Copy)] pub struct TypeHint { - /// The type hint annotation - #[doc(hidden)] - pub annotation: &'static str, - /// The modules to import - #[doc(hidden)] - pub imports: &'static [TypeHintImport], + inner: TypeHintExpr, } -/// `from {module} import {name}` import -#[doc(hidden)] #[derive(Clone, Copy)] -pub struct TypeHintImport { - /// The module from which to import - #[doc(hidden)] - pub module: &'static str, - /// The elements to import from the module - #[doc(hidden)] - pub name: &'static str, +enum TypeHintExpr { + /// A built-name like `list` or `datetime`. Used for built-in types or modules. + Builtin { id: &'static str }, + /// A module member like `datetime.time` where module = `datetime` and attr = `time` + ModuleAttribute { + module: &'static str, + attr: &'static str, + }, + /// A union elts[0] | ... | elts[len] + Union { elts: &'static [TypeHint] }, + /// A subscript main[*args] + Subscript { + value: &'static TypeHint, + slice: &'static [TypeHint], + }, } -impl fmt::Display for TypeHint { - #[inline] - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - self.annotation.fmt(f) +impl TypeHint { + /// A builtin like `int` or `list` + /// + /// ``` + /// use pyo3::inspect::TypeHint; + /// + /// const T: TypeHint = TypeHint::builtin("int"); + /// assert_eq!(T.to_string(), "int"); + /// ``` + pub const fn builtin(name: &'static str) -> Self { + Self { + inner: TypeHintExpr::Builtin { id: name }, + } } -} -/// Allows to build a [`TypeHint`] from a module name and a qualified name -/// -/// ``` -/// use pyo3::type_hint; -/// use pyo3::inspect::TypeHint; -/// -/// const T: TypeHint = type_hint!("collections.abc", "Sequence"); -/// assert_eq!(T.to_string(), "Sequence"); -/// ``` -#[macro_export] -macro_rules! type_hint { - ($qualname: expr) => { - $crate::inspect::TypeHint { - annotation: $qualname, - imports: &[], + /// A type contained in a module like `datetime.time` + /// + /// ``` + /// use pyo3::inspect::TypeHint; + /// + /// const T: TypeHint = TypeHint::module_member("datetime", "time"); + /// assert_eq!(T.to_string(), "datetime.time"); + /// ``` + pub const fn module_member(module: &'static str, attr: &'static str) -> Self { + Self { + inner: TypeHintExpr::ModuleAttribute { module, attr }, } - }; - ($module:expr, $name: expr) => { - $crate::inspect::TypeHint { - annotation: $name, - imports: &[$crate::inspect::TypeHintImport { - module: $module, - name: $name, - }], + } + + /// The union of multiple types + /// + /// ``` + /// use pyo3::inspect::TypeHint; + /// + /// const T: TypeHint = TypeHint::union(&[TypeHint::builtin("int"), TypeHint::builtin("float")]); + /// assert_eq!(T.to_string(), "int | float"); + /// ``` + pub const fn union(elts: &'static [TypeHint]) -> Self { + Self { + inner: TypeHintExpr::Union { elts }, } - }; -} + } -/// Allows to build a [`TypeHint`] that is the union of other [`TypeHint`] -/// -/// ``` -/// use pyo3::{type_hint, type_hint_union}; -/// use pyo3::inspect::TypeHint; -/// -/// const T: TypeHint = type_hint_union!(type_hint!("a", "A"), type_hint!("b", "B")); -/// assert_eq!(T.to_string(), "A | B"); -/// ``` -#[macro_export] -macro_rules! type_hint_union { - // TODO: avoid using the parameters twice - // TODO: factor our common code in const functions - ($arg:expr) => { $arg }; - ($firstarg:expr, $($arg:expr),+) => {{ - $crate::inspect::TypeHint { - annotation: { - const PARTS: &[&[u8]] = &[$firstarg.annotation.as_bytes(), $(b" | ", $arg.annotation.as_bytes()),*]; - unsafe { - ::std::str::from_utf8_unchecked(&$crate::impl_::concat::combine_to_array::<{ - $crate::impl_::concat::combined_len(PARTS) - }>(PARTS)) + /// A subscribed type, often a container + /// + /// ``` + /// use pyo3::inspect::TypeHint; + /// + /// const T: TypeHint = TypeHint::subscript(&TypeHint::builtin("dict"), &[TypeHint::builtin("int"), TypeHint::builtin("str")]); + /// assert_eq!(T.to_string(), "dict[int, str]"); + /// ``` + pub const fn subscript(value: &'static Self, slice: &'static [Self]) -> Self { + Self { + inner: TypeHintExpr::Subscript { value, slice }, + } + } + + /// Serialize the type for introspection + /// + /// We use the same AST as Python: https://docs.python.org/3/library/ast.html#abstract-grammar + #[doc(hidden)] + #[allow(clippy::incompatible_msrv)] // The introspection feature target 1.83+ + pub const fn serialize_for_introspection(&self, mut output: &mut [u8]) { + match &self.inner { + TypeHintExpr::Builtin { id } => { + output = write_slice_and_move_forward(b"{\"type\":\"builtin\",\"id\":\"", output); + output = write_slice_and_move_forward(id.as_bytes(), output); + write_slice_and_move_forward(b"\"}", output); + } + TypeHintExpr::ModuleAttribute { module, attr } => { + output = + write_slice_and_move_forward(b"{\"type\":\"attribute\",\"module\":\"", output); + output = write_slice_and_move_forward(module.as_bytes(), output); + output = write_slice_and_move_forward(b"\",\"attr\":\"", output); + output = write_slice_and_move_forward(attr.as_bytes(), output); + write_slice_and_move_forward(b"\"}", output); + } + TypeHintExpr::Union { elts } => { + output = write_slice_and_move_forward(b"{\"type\":\"union\",\"elts\":[", output); + let mut i = 0; + while i < elts.len() { + if i > 0 { + output = write_slice_and_move_forward(b",", output); + } + output = write_type_hind_and_move_forward(&elts[i], output); + i += 1; + } + write_slice_and_move_forward(b"]}", output); + } + TypeHintExpr::Subscript { value, slice } => { + output = + write_slice_and_move_forward(b"{\"type\":\"subscript\",\"value\":", output); + output = write_type_hind_and_move_forward(value, output); + output = write_slice_and_move_forward(b",\"slice\":[", output); + let mut i = 0; + while i < slice.len() { + if i > 0 { + output = write_slice_and_move_forward(b",", output); + } + output = write_type_hind_and_move_forward(&slice[i], output); + i += 1; } - }, - imports: { - const ARGS: &[$crate::inspect::TypeHint] = &[$firstarg, $($arg),*]; - const LEN: usize = { - let mut count = 0; - let mut i = 0; - while i < ARGS.len() { - count += ARGS[i].imports.len(); - i += 1; + write_slice_and_move_forward(b"]}", output); + } + } + } + + /// Length required by [`Self::serialize_for_introspection`] + #[doc(hidden)] + pub const fn serialized_len_for_introspection(&self) -> usize { + match &self.inner { + TypeHintExpr::Builtin { id } => 26 + id.len(), + TypeHintExpr::ModuleAttribute { module, attr } => 42 + module.len() + attr.len(), + TypeHintExpr::Union { elts } => { + let mut count = 26; + let mut i = 0; + while i < elts.len() { + if i > 0 { + count += 1; } - count - }; - const OUTPUT: [$crate::inspect::TypeHintImport; LEN] = { - let mut output = [$crate::inspect::TypeHintImport { module: "", name: "" }; LEN]; - let mut args_i = 0; - let mut in_arg_i = 0; - let mut output_i = 0; - while args_i < ARGS.len() { - while in_arg_i < ARGS[args_i].imports.len() { - output[output_i] = ARGS[args_i].imports[in_arg_i]; - in_arg_i += 1; - output_i += 1; - } - args_i += 1; - in_arg_i = 0; + count += elts[i].serialized_len_for_introspection(); + i += 1; + } + count + } + TypeHintExpr::Subscript { value, slice } => { + let mut count = 40 + value.serialized_len_for_introspection(); + let mut i = 0; + while i < slice.len() { + if i > 0 { + count += 1; } - output - }; - &OUTPUT + count += slice[i].serialized_len_for_introspection(); + i += 1; + } + count } } - }}; + } } -/// Allows to build a [`TypeHint`] that is the subscripted -/// -/// ``` -/// use pyo3::{type_hint, type_hint_subscript}; -/// use pyo3::inspect::TypeHint; -/// -/// const T: TypeHint = type_hint_subscript!(type_hint!("collections.abc", "Sequence"), type_hint!("weakref", "ProxyType")); -/// assert_eq!(T.to_string(), "Sequence[ProxyType]"); -/// ``` -#[macro_export] -macro_rules! type_hint_subscript { - // TODO: avoid using the parameters twice - // TODO: factor our common code in const functions - ($main:expr, $firstarg:expr $(, $arg:expr)*) => {{ - $crate::inspect::TypeHint { - annotation: { - const PARTS: &[&[u8]] = &[$main.annotation.as_bytes(), b"[", $firstarg.annotation.as_bytes() $(, b", ", $arg.annotation.as_bytes())* , b"]"]; - unsafe { - ::std::str::from_utf8_unchecked(&$crate::impl_::concat::combine_to_array::<{ - $crate::impl_::concat::combined_len(PARTS) - }>(PARTS)) - } - }, - imports: { - const ARGS: &[$crate::inspect::TypeHint] = &[$main, $firstarg, $($arg),*]; - const LEN: usize = { - let mut count = 0; - let mut i = 0; - while i < ARGS.len() { - count += ARGS[i].imports.len(); - i += 1; +impl fmt::Display for TypeHint { + #[inline] + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match &self.inner { + TypeHintExpr::Builtin { id } => id.fmt(f), + TypeHintExpr::ModuleAttribute { module, attr } => { + module.fmt(f)?; + f.write_str(".")?; + attr.fmt(f) + } + TypeHintExpr::Union { elts } => { + for (i, elt) in elts.iter().enumerate() { + if i > 0 { + f.write_str(" | ")?; } - count - }; - const OUTPUT: [$crate::inspect::TypeHintImport; LEN] = { - let mut output = [$crate::inspect::TypeHintImport { module: "", name: "" }; LEN]; - let mut args_i = 0; - let mut in_arg_i = 0; - let mut output_i = 0; - while args_i < ARGS.len() { - while in_arg_i < ARGS[args_i].imports.len() { - output[output_i] = ARGS[args_i].imports[in_arg_i]; - in_arg_i += 1; - output_i += 1; - } - args_i += 1; - in_arg_i = 0; + elt.fmt(f)?; + } + Ok(()) + } + TypeHintExpr::Subscript { value, slice } => { + value.fmt(f)?; + f.write_str("[")?; + for (i, elt) in slice.iter().enumerate() { + if i > 0 { + f.write_str(", ")?; } - output - }; - &OUTPUT + elt.fmt(f)?; + } + f.write_str("]") } } - }}; + } +} + +#[allow(clippy::incompatible_msrv)] // The experimental-inspect feature is targeting 1.83+ +const fn write_slice_and_move_forward<'a>(value: &[u8], output: &'a mut [u8]) -> &'a mut [u8] { + let mut i = 0; + while i < value.len() { + output[i] = value[i]; + i += 1; + } + output.split_at_mut(value.len()).1 +} + +#[allow(clippy::incompatible_msrv)] // The experimental-inspect feature is targeting 1.83+ +const fn write_type_hind_and_move_forward<'a>( + value: &TypeHint, + output: &'a mut [u8], +) -> &'a mut [u8] { + value.serialize_for_introspection(output); + output + .split_at_mut(value.serialized_len_for_introspection()) + .1 } #[cfg(test)] @@ -193,25 +241,35 @@ mod tests { use super::*; #[test] - fn test_union() { - const T: TypeHint = type_hint_union!(type_hint!("a", "A"), type_hint!("b", "B")); - assert_eq!(T.annotation, "A | B"); - assert_eq!(T.imports[0].name, "A"); - assert_eq!(T.imports[0].module, "a"); - assert_eq!(T.imports[1].name, "B"); - assert_eq!(T.imports[1].module, "b"); + fn test_to_string() { + const T: TypeHint = TypeHint::subscript( + &TypeHint::builtin("dict"), + &[ + TypeHint::union(&[TypeHint::builtin("int"), TypeHint::builtin("float")]), + TypeHint::module_member("datetime", "time"), + ], + ); + assert_eq!(T.to_string(), "dict[int | float, datetime.time]") } #[test] - fn test_subscript() { - const T: TypeHint = type_hint_subscript!( - type_hint!("collections.abc", "Sequence"), - type_hint!("weakref", "ProxyType") + fn test_serialize_for_introspection() { + const T: TypeHint = TypeHint::subscript( + &TypeHint::builtin("dict"), + &[ + TypeHint::union(&[TypeHint::builtin("int"), TypeHint::builtin("float")]), + TypeHint::module_member("datetime", "time"), + ], ); - assert_eq!(T.annotation, "Sequence[ProxyType]"); - assert_eq!(T.imports[0].name, "Sequence"); - assert_eq!(T.imports[0].module, "collections.abc"); - assert_eq!(T.imports[1].name, "ProxyType"); - assert_eq!(T.imports[1].module, "weakref"); + const SER_LEN: usize = T.serialized_len_for_introspection(); + const SER: [u8; SER_LEN] = { + let mut out: [u8; SER_LEN] = [0; SER_LEN]; + T.serialize_for_introspection(&mut out); + out + }; + assert_eq!( + str::from_utf8(&SER).unwrap(), + r#"{"type":"subscript","value":{"type":"builtin","id":"dict"},"slice":[{"type":"union","elts":[{"type":"builtin","id":"int"},{"type":"builtin","id":"float"}]},{"type":"attribute","module":"datetime","attr":"time"}]}"# + ) } } diff --git a/src/type_object.rs b/src/type_object.rs index f8fab527da9..c5d54169e88 100644 --- a/src/type_object.rs +++ b/src/type_object.rs @@ -3,8 +3,6 @@ use crate::ffi_ptr_ext::FfiPtrExt; #[cfg(feature = "experimental-inspect")] use crate::inspect::TypeHint; -#[cfg(feature = "experimental-inspect")] -use crate::type_hint; use crate::types::{PyAny, PyType}; use crate::{ffi, Bound, Python}; use std::ptr; @@ -48,7 +46,7 @@ pub unsafe trait PyTypeInfo: Sized { /// Provides the full python type as a type hint. #[cfg(feature = "experimental-inspect")] - const TYPE_HINT: TypeHint = type_hint!("typing", "Any"); + const TYPE_HINT: TypeHint = TypeHint::module_member("typing", "Any"); /// Returns the PyTypeObject instance for this type. fn type_object_raw(py: Python<'_>) -> *mut ffi::PyTypeObject; diff --git a/src/types/boolobject.rs b/src/types/boolobject.rs index 518c0642b08..de31685dc4f 100644 --- a/src/types/boolobject.rs +++ b/src/types/boolobject.rs @@ -4,8 +4,6 @@ use crate::conversion::IntoPyObject; use crate::inspect::types::TypeInfo; #[cfg(feature = "experimental-inspect")] use crate::inspect::TypeHint; -#[cfg(feature = "experimental-inspect")] -use crate::type_hint; use crate::PyErr; use crate::{ exceptions::PyTypeError, ffi, ffi_ptr_ext::FfiPtrExt, instance::Bound, @@ -146,7 +144,7 @@ impl<'py> IntoPyObject<'py> for bool { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: TypeHint = type_hint!("bool"); + const OUTPUT_TYPE: TypeHint = TypeHint::builtin("bool"); #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { @@ -185,7 +183,7 @@ impl FromPyObject<'_, '_> for bool { type Error = PyErr; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: TypeHint = type_hint!("bool"); + const INPUT_TYPE: TypeHint = TypeHint::builtin("bool"); fn extract(obj: Borrowed<'_, '_, PyAny>) -> Result { let err = match obj.cast::() { diff --git a/src/types/float.rs b/src/types/float.rs index 5c70a19f1a4..5a3e22802aa 100644 --- a/src/types/float.rs +++ b/src/types/float.rs @@ -3,8 +3,6 @@ use crate::conversion::IntoPyObject; use crate::inspect::types::TypeInfo; #[cfg(feature = "experimental-inspect")] use crate::inspect::TypeHint; -#[cfg(feature = "experimental-inspect")] -use crate::type_hint; use crate::{ ffi, ffi_ptr_ext::FfiPtrExt, instance::Bound, Borrowed, FromPyObject, PyAny, PyErr, Python, }; @@ -77,7 +75,7 @@ impl<'py> IntoPyObject<'py> for f64 { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: TypeHint = type_hint!("float"); + const OUTPUT_TYPE: TypeHint = TypeHint::builtin("float"); #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { @@ -113,7 +111,7 @@ impl<'py> FromPyObject<'_, 'py> for f64 { type Error = PyErr; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: TypeHint = type_hint!("float"); + const INPUT_TYPE: TypeHint = TypeHint::builtin("float"); // PyFloat_AsDouble returns -1.0 upon failure #[allow(clippy::float_cmp)] @@ -150,7 +148,7 @@ impl<'py> IntoPyObject<'py> for f32 { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: TypeHint = type_hint!("float"); + const OUTPUT_TYPE: TypeHint = TypeHint::builtin("float"); #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { @@ -186,7 +184,7 @@ impl<'a, 'py> FromPyObject<'a, 'py> for f32 { type Error = >::Error; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: TypeHint = type_hint!("float"); + const INPUT_TYPE: TypeHint = TypeHint::builtin("float"); fn extract(obj: Borrowed<'_, 'py, PyAny>) -> Result { Ok(obj.extract::()? as f32) diff --git a/src/types/weakref/anyref.rs b/src/types/weakref/anyref.rs index 9639fb96e3d..60e1fe877ab 100644 --- a/src/types/weakref/anyref.rs +++ b/src/types/weakref/anyref.rs @@ -4,9 +4,9 @@ use crate::ffi_ptr_ext::FfiPtrExt; use crate::inspect::TypeHint; use crate::type_object::{PyTypeCheck, PyTypeInfo}; use crate::types::any::PyAny; -use crate::{ffi, Bound}; #[cfg(feature = "experimental-inspect")] -use crate::{type_hint, type_hint_union}; +use crate::types::{PyWeakrefProxy, PyWeakrefReference}; +use crate::{ffi, Bound}; /// Represents any Python `weakref` reference. /// @@ -23,11 +23,10 @@ pyobject_native_type_named!(PyWeakref); impl PyTypeCheck for PyWeakref { const NAME: &'static str = "weakref"; #[cfg(feature = "experimental-inspect")] - const TYPE_HINT: TypeHint = type_hint_union!( - type_hint!("weakref", "ProxyType"), - type_hint!("weakref", "CallableProxyType"), - type_hint!("weakref", "ReferenceType") - ); + const TYPE_HINT: TypeHint = TypeHint::union(&[ + PyWeakrefProxy::TYPE_HINT, + ::TYPE_HINT, + ]); 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 6b456a26e07..5bc31bffcb7 100644 --- a/src/types/weakref/proxy.rs +++ b/src/types/weakref/proxy.rs @@ -7,8 +7,6 @@ use crate::py_result_ext::PyResultExt; use crate::type_object::PyTypeCheck; use crate::types::any::PyAny; use crate::{ffi, Borrowed, Bound, BoundObject, IntoPyObject, IntoPyObjectExt}; -#[cfg(feature = "experimental-inspect")] -use crate::{type_hint, type_hint_union}; /// Represents any Python `weakref` Proxy type. /// @@ -26,10 +24,10 @@ pyobject_native_type_named!(PyWeakrefProxy); impl PyTypeCheck for PyWeakrefProxy { const NAME: &'static str = "weakref.ProxyTypes"; #[cfg(feature = "experimental-inspect")] - const TYPE_HINT: TypeHint = type_hint_union!( - type_hint!("weakref", "ProxyType"), - type_hint!("weakref", "CallableProxyType") - ); + const TYPE_HINT: TypeHint = TypeHint::union(&[ + TypeHint::module_member("weakref", "ProxyType"), + TypeHint::module_member("weakref", "CallableProxyType"), + ]); fn type_check(object: &Bound<'_, PyAny>) -> bool { unsafe { ffi::PyWeakref_CheckProxy(object.as_ptr()) > 0 }