diff --git a/Cargo.toml b/Cargo.toml index c47ecf0affb..3a5797b255f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -156,7 +156,7 @@ full = [ "chrono-tz", "either", "experimental-async", - "experimental-inspect", + # "experimental-inspect", # Requires Rust 1.83+ "eyre", "hashbrown", "indexmap", diff --git a/newsfragments/5438.changed.md b/newsfragments/5438.changed.md new file mode 100644 index 00000000000..bcd7885673d --- /dev/null +++ b/newsfragments/5438.changed.md @@ -0,0 +1,2 @@ +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 274fea9722e..9da560985c5 100644 --- a/noxfile.py +++ b/noxfile.py @@ -1097,7 +1097,15 @@ def test_introspection(session: nox.Session): profile = os.environ.get("CARGO_BUILD_PROFILE") if profile == "release": options.append("--release") - 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.*"): @@ -1157,6 +1165,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/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..ea57fdaf814 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, TypeHintExpr, + VariableLengthArgument, }; use anyhow::{anyhow, bail, ensure, Context, Result}; use goblin::elf::section_header::SHN_XINDEX; @@ -9,11 +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::Deserialize; +use serde::de::value::MapAccessDeserializer; +use serde::de::{Error, MapAccess, Visitor}; +use serde::{Deserialize, Deserializer}; use std::cmp::Ordering; use std::collections::HashMap; use std::path::Path; -use std::{fs, str}; +use std::{fmt, fs, str}; /// Introspect a cdylib built with PyO3 and returns the definition of a Python module. /// @@ -192,7 +195,7 @@ fn convert_function( name: &str, arguments: &ChunkArguments, decorators: &[String], - returns: &Option, + returns: &Option, ) -> Function { Function { name: name.into(), @@ -210,7 +213,7 @@ fn convert_function( .as_ref() .map(convert_variable_length_argument), }, - returns: returns.clone(), + returns: returns.as_ref().map(convert_type_hint), } } @@ -218,22 +221,50 @@ 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 { + match arg { + ChunkTypeHint::Ast(expr) => TypeHint::Ast(convert_type_hint_expr(expr)), + ChunkTypeHint::Plain(t) => TypeHint::Plain(t.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(), + }, } } @@ -388,8 +419,8 @@ enum Chunk { parent: Option, #[serde(default)] decorators: Vec, - #[serde(default)] - returns: Option, + #[serde(default, deserialize_with = "deserialize_type_hint")] + returns: Option, }, Attribute { #[serde(default)] @@ -399,8 +430,8 @@ enum Chunk { name: String, #[serde(default)] value: Option, - #[serde(default)] - annotation: Option, + #[serde(default, deserialize_with = "deserialize_type_hint")] + annotation: Option, }, } @@ -423,6 +454,69 @@ struct ChunkArgument { name: String, #[serde(default)] default: Option, - #[serde(default)] - annotation: Option, + #[serde(default, deserialize_with = "deserialize_type_hint")] + annotation: Option, +} + +/// Variant of [`TypeHint`] that implements deserialization. +/// +/// We keep separated type to allow them to evolve independently (this type will need to handle backward compatibility). +enum ChunkTypeHint { + Ast(ChunkTypeHintExpr), + Plain(String), +} + +#[derive(Deserialize)] +#[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_type_hint<'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::Plain(v)) + } + + fn visit_map>(self, map: M) -> Result { + Ok(ChunkTypeHint::Ast(Deserialize::deserialize( + 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..5d13d15c51b 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,30 @@ 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 +/// +/// Might be a plain string or an AST fragment +#[derive(Debug, Eq, PartialEq, Clone, Hash)] +pub enum TypeHint { + Ast(TypeHintExpr), + Plain(String), +} + +/// A type hint annotation as an AST fragment +#[derive(Debug, Eq, PartialEq, Clone, Hash)] +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 baad91dd6e2..d66d899ce00 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, VariableLengthArgument, + Argument, Arguments, Attribute, Class, Function, Module, TypeHint, TypeHintExpr, + 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; +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. @@ -11,40 +12,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 imports = Imports::create(module, parents); let mut elements = Vec::new(); for attribute in &module.attributes { - elements.push(attribute_stubs(attribute, &mut modules_to_import)); + elements.push(attribute_stubs(attribute, &imports)); } for class in &module.classes { - elements.push(class_stubs(class, &mut modules_to_import)); + elements.push(class_stubs(class, &imports)); } for function in &module.functions { - elements.push(function_stubs(function, &mut modules_to_import)); + elements.push(function_stubs(function, &imports)); } // We generate a __getattr__ method to tag incomplete stubs @@ -59,22 +69,22 @@ fn module_stubs(module: &Module) -> String { arguments: vec![Argument { name: "name".to_string(), default_value: None, - annotation: Some("str".into()), + annotation: Some(TypeHint::Ast(TypeHintExpr::Builtin { id: "str".into() })), }], vararg: None, keyword_only_arguments: Vec::new(), kwarg: None, }, - returns: Some("_typeshed.Incomplete".into()), + returns: Some(TypeHint::Ast(TypeHintExpr::Attribute { + module: "_typeshed".into(), + attr: "Incomplete".into(), + })), }, - &mut modules_to_import, + &imports, )); } - let mut final_elements = Vec::new(); - for module_to_import in &modules_to_import { - final_elements.push(format!("import {module_to_import}")); - } + let mut final_elements = imports.imports; final_elements.extend(elements); let mut output = String::new(); @@ -99,7 +109,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: &Imports) -> String { let mut buffer = format!("class {}:", class.name); if class.methods.is_empty() && class.attributes.is_empty() { buffer.push_str(" ..."); @@ -108,43 +118,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: &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,150 +170,327 @@ 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)); + type_hint_stub(returns, imports, &mut buffer); } buffer.push_str(": ..."); buffer } -fn attribute_stubs(attribute: &Attribute, modules_to_import: &mut BTreeSet) -> 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(annotation_stub(annotation, modules_to_import)); + 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, modules_to_import: &mut BTreeSet) -> 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(annotation_stub(annotation, modules_to_import)); + 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, - modules_to_import: &mut BTreeSet, -) -> 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(annotation_stub(annotation, modules_to_import)); + buffer.push_str(": "); + type_hint_stub(annotation, imports, &mut buffer); } - output + buffer } -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(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 } -// 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 +#[derive(Default)] +struct Imports { + /// 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>, } -// 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; - } - 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; +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 { + // We split nested classes A.B in "A" (the part that must be imported and can have naming conflicts) and ".B" + let (root_attr, attr_path) = attr + .split_once('.') + .map_or((attr.as_str(), None), |(root, path)| (root, Some(path))); + let mut local_name = root_attr.to_owned(); + let mut already_imported = false; + 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 == root_attr + { + // It's the same + already_imported = true; + break; + } + // We generate a new local name + // TODO: we use currently a format like Foo2. It might be nicer to use something like ModFoo + 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 + ); + } + renaming.insert( + (module.clone(), attr.clone()), + if let Some(attr_path) = attr_path { + format!("{local_name}.{attr_path}") + } else { + local_name.clone() + }, + ); + if !already_imported { + local_name_to_module_and_attribute.insert( + local_name.clone(), + (normalized_module.clone(), root_attr.to_owned()), + ); + import_for_module.push(if local_name == root_attr { + local_name + } else { + format!("{root_attr} as {local_name}") + }); + } + } + if let Some(module) = normalized_module { + imports.push(format!( + "from {module} import {}", + import_for_module.join(", ") + )); + } + } + + Self { imports, renaming } } - Some(&input[0..length]) -} -#[cfg(test)] -mod tests { - use super::*; - use crate::model::Arguments; + 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(']'); + } + } + } +} - #[test] - fn annotation_stub_proper_imports() { - let mut modules_to_import = BTreeSet::new(); +/// Lists all the elements used in annotations +struct ElementsUsedInAnnotations { + /// module -> name + module_members: BTreeMap>, + builtins: BTreeSet, +} - // Basic int - annotation_stub("int", &mut modules_to_import); - assert!(modules_to_import.is_empty()); +impl ElementsUsedInAnnotations { + fn new() -> Self { + Self { + module_members: BTreeMap::new(), + builtins: BTreeSet::new(), + } + } - // Simple path - annotation_stub("collections.abc.Iterable", &mut modules_to_import); - assert!(modules_to_import.contains("collections.abc")); + 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()); + } + } - // With underscore - annotation_stub("_foo._bar_baz", &mut modules_to_import); - assert!(modules_to_import.contains("_foo")); + 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); + } + } - // Basic generic - annotation_stub("typing.List[int]", &mut modules_to_import); - assert!(modules_to_import.contains("typing")); + fn walk_attribute(&mut self, attribute: &Attribute) { + if let Some(type_hint) = &attribute.annotation { + self.walk_type_hint(type_hint); + } + } - // Complex generic - annotation_stub("typing.List[foo.Bar[int]]", &mut modules_to_import); - assert!(modules_to_import.contains("foo")); + 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); + } + } - // 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")); + fn walk_type_hint(&mut self, type_hint: &TypeHint) { + if let TypeHint::Ast(type_hint) = type_hint { + self.walk_type_hint_expr(type_hint); + } + } - // Union - annotation_stub("a.B | b.C", &mut modules_to_import); - assert!(modules_to_import.contains("a")); - assert!(modules_to_import.contains("b")); + 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); + } + } + } } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::model::Arguments; #[test] fn function_stubs_with_variable_length() { @@ -328,18 +515,18 @@ mod tests { keyword_only_arguments: vec![Argument { name: "karg".into(), default_value: None, - annotation: Some("str".into()), + annotation: Some(TypeHint::Plain("str".into())), }], kwarg: Some(VariableLengthArgument { name: "kwarg".into(), - annotation: Some("str".into()), + annotation: Some(TypeHint::Plain("str".into())), }), }, - returns: Some("list[str]".into()), + returns: Some(TypeHint::Plain("list[str]".into())), }; assert_eq!( "def func(posonly, /, arg, *varargs, karg: str, **kwarg: str) -> list[str]: ...", - function_stubs(&function, &mut BTreeSet::new()) + function_stubs(&function, &Imports::default()) ) } @@ -363,7 +550,7 @@ mod tests { keyword_only_arguments: vec![Argument { name: "karg".into(), default_value: Some("\"foo\"".into()), - annotation: Some("str".into()), + annotation: Some(TypeHint::Plain("str".into())), }], kwarg: None, }, @@ -371,7 +558,81 @@ mod tests { }; assert_eq!( "def afunc(posonly=1, /, arg=True, *, karg: str = \"foo\"): ...", - function_stubs(&function, &mut BTreeSet::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.C".into(), + }, + TypeHintExpr::Attribute { + module: "foo".into(), + attr: "A.D".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.C | A3.D | B | A2]"); + } } diff --git a/pyo3-macros-backend/src/frompyobject.rs b/pyo3-macros-backend/src/frompyobject.rs index d3232e90777..841fcd2039f 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,12 +100,11 @@ 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 variants = self.variants.iter().map(|var| var.input_type(ctx)); + quote! { + #pyo3_crate_path::inspect::TypeHint::union(&[#(#variants),*]) } } } @@ -458,48 +457,42 @@ 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) + }); + quote! { #pyo3_crate_path::inspect::TypeHint::subscript(&#pyo3_crate_path::inspect::TypeHint::builtin("tuple"), &[#(#elements),*]) } } ContainerType::Struct(_) => { // TODO: implement using a Protocol? - builder.push_str("_typeshed.Incomplete") + quote! { #pyo3_crate_path::inspect::TypeHint::module_attr("_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::inspect::TypeHint::module_attr("_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 +562,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::inspect::TypeHint::module_attr("_typeshed", "Incomplete") } } } } else { // We don't know how to deal with generic parameters // Blocked by https://github.com/rust-lang/rust/issues/76560 - builder.push_str("_typeshed.Incomplete") + quote! { #pyo3_crate_path::inspect::TypeHint::module_attr("_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..75dfe054fa7 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,44 @@ 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, + let elements = tups.iter().map( + |TupleStructField { + into_py_with, + field, + }| { + Self::field_output_type(into_py_with, &field.ty, ctx) }, - ) 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("]"); + ); + quote! { #pyo3_crate_path::inspect::TypeHint::subscript(&#pyo3_crate_path::inspect::TypeHint::builtin("tuple"), &[#(#elements),*]) } } ContainerType::Struct(_) => { // TODO: implement using a Protocol? - builder.push_str("_typeshed.Incomplete") + quote! { #pyo3_crate_path::inspect::TypeHint::module_attr("_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::inspect::TypeHint::module_attr("_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,12 +477,11 @@ 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 variants = self.variants.iter().map(|var| var.output_type(ctx)); + quote! { + #pyo3_crate_path::inspect::TypeHint::union(&[#(#variants),*]) } } } @@ -587,17 +578,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 +595,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::inspect::TypeHint::module_attr("_typeshed", "Incomplete") } } } } else { // We don't know how to deal with generic parameters // Blocked by https://github.com/rust-lang/rust/issues/76560 - builder.push_str("_typeshed.Incomplete") + quote! { #pyo3_crate_path::inspect::TypeHint::module_attr("_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..e35b77e87cf 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::inspect::TypeHint::union(&[#annotation, #pyo3_crate_path::inspect::TypeHint::builtin("None")]) }; } - content.push_str("\""); + content.push_tokens(serialize_type_hint(annotation, pyo3_crate_path)); } Self::OutputType { rust_type, is_final, } => { - content.push_str("\""); + let mut annotation = quote! { <#rust_type as #pyo3_crate_path::impl_::introspection::PyReturnType>::OUTPUT_TYPE }; if is_final { - content.push_str("typing.Final["); + annotation = quote! { #pyo3_crate_path::inspect::TypeHint::subscript(&#pyo3_crate_path::inspect::TypeHint::module_attr("typing", "Final"), &[#annotation]) }; } - content.push_tokens(quote! { <#rust_type as #pyo3_crate_path::impl_::introspection::PyReturnType>::OUTPUT_TYPE.as_bytes() }); - if is_final { - content.push_str("]"); - } - content.push_str("\""); + 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::inspect::TypeHint::module_attr(#module, #name) } + } else { + quote! { #pyo3_crate_path::inspect::TypeHint::builtin(#name) } + }; + content.push_tokens(serialize_type_hint(annotation, pyo3_crate_path)); } Self::Map(map) => { content.push_str("{"); @@ -450,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 0f8eea038d9..b9318015d30 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::inspect::TypeHint::module_attr(#module, #name) } } else { - name.to_string() + quote! { #pyo3_path::inspect::TypeHint::builtin(#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/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/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 fbb1eae2150..966173c653e 100644 --- a/src/conversion.rs +++ b/src/conversion.rs @@ -2,6 +2,8 @@ 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}; use crate::types::PyTuple; @@ -59,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: &'static str = "typing.Any"; + const OUTPUT_TYPE: TypeHint = TypeHint::module_attr("typing", "Any"); /// Performs the conversion. fn into_pyobject(self, py: Python<'py>) -> Result; @@ -193,7 +195,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 +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: &'static str = "typing.Any"; + const INPUT_TYPE: TypeHint = TypeHint::module_attr("typing", "Any"); /// Extracts `Self` from the bound smart pointer `obj`. /// @@ -534,7 +536,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 +550,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 +567,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..e08b989c00f 100644 --- a/src/conversions/std/num.rs +++ b/src/conversions/std/num.rs @@ -3,6 +3,8 @@ 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; use crate::types::{PyByteArray, PyByteArrayMethods, PyBytes, PyInt}; use crate::{exceptions, ffi, Borrowed, Bound, FromPyObject, PyAny, PyErr, PyResult, Python}; use std::convert::Infallible; @@ -23,7 +25,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 +43,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 +59,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 +108,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 = TypeHint::builtin("int"); fn into_pyobject(self, py: Python<'py>) -> Result { unsafe { @@ -127,7 +129,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 +140,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 = 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) @@ -160,7 +162,7 @@ macro_rules! int_fits_c_long { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = "int"; + const OUTPUT_TYPE: TypeHint = TypeHint::builtin("int"); fn into_pyobject(self, py: Python<'py>) -> Result { unsafe { @@ -182,7 +184,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 +201,7 @@ macro_rules! int_fits_c_long { type Error = PyErr; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: &'static str = "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)?; @@ -221,7 +223,7 @@ impl<'py> IntoPyObject<'py> for u8 { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = "int"; + const OUTPUT_TYPE: TypeHint = TypeHint::builtin("int"); fn into_pyobject(self, py: Python<'py>) -> Result { unsafe { @@ -255,7 +257,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 +286,7 @@ impl<'py> FromPyObject<'_, 'py> for u8 { type Error = PyErr; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: &'static str = "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)?; @@ -432,7 +434,7 @@ mod fast_128bit_int_conversion { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = "int"; + const OUTPUT_TYPE: TypeHint = TypeHint::builtin("int"); fn into_pyobject(self, py: Python<'py>) -> Result { #[cfg(not(Py_3_13))] @@ -489,7 +491,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 +508,7 @@ mod fast_128bit_int_conversion { type Error = PyErr; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: &'static str = "int"; + const INPUT_TYPE: TypeHint = TypeHint::builtin("int"); fn extract(ob: Borrowed<'_, '_, PyAny>) -> Result<$rust_type, Self::Error> { let num = @@ -582,7 +584,7 @@ mod slow_128bit_int_conversion { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = "int"; + const OUTPUT_TYPE: TypeHint = TypeHint::builtin("int"); fn into_pyobject(self, py: Python<'py>) -> Result { let lower = (self as u64).into_pyobject(py)?; @@ -610,7 +612,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 +629,7 @@ mod slow_128bit_int_conversion { type Error = PyErr; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: &'static str = "int"; + const INPUT_TYPE: TypeHint = TypeHint::builtin("int"); fn extract(ob: Borrowed<'_, '_, PyAny>) -> Result<$rust_type, Self::Error> { let py = ob.py(); @@ -681,7 +683,7 @@ macro_rules! nonzero_int_impl { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = "int"; + const OUTPUT_TYPE: TypeHint = TypeHint::builtin("int"); #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { @@ -700,7 +702,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 +719,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..f46c159967d 100644 --- a/src/conversions/std/string.rs +++ b/src/conversions/std/string.rs @@ -1,11 +1,12 @@ -use std::{borrow::Cow, convert::Infallible}; - #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; +#[cfg(feature = "experimental-inspect")] +use crate::inspect::TypeHint; 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 +14,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 +33,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 +52,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 +71,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 +90,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 +109,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 +128,7 @@ impl<'py> IntoPyObject<'py> for String { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = "str"; + const OUTPUT_TYPE: TypeHint = TypeHint::builtin("str"); fn into_pyobject(self, py: Python<'py>) -> Result { Ok(PyString::new(py, &self)) @@ -145,7 +146,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 +164,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 = TypeHint::builtin("str"); fn extract(ob: crate::Borrowed<'a, '_, PyAny>) -> Result { ob.cast::()?.to_str() @@ -179,7 +180,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 = TypeHint::builtin("str"); fn extract(ob: crate::Borrowed<'a, '_, PyAny>) -> Result { ob.cast::()?.to_cow() @@ -197,7 +198,7 @@ impl FromPyObject<'_, '_> for String { type Error = PyErr; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: &'static str = "str"; + const INPUT_TYPE: TypeHint = TypeHint::builtin("str"); fn extract(obj: Borrowed<'_, '_, PyAny>) -> Result { obj.cast::()?.to_cow().map(Cow::into_owned) @@ -213,7 +214,7 @@ impl FromPyObject<'_, '_> for char { type Error = PyErr; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: &'static str = "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 424b3ef998f..132100c202a 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, @@ -60,7 +62,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 +78,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 +94,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 +111,10 @@ where type Error = T::Error; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: &'static str = "typing.Any | None"; + const INPUT_TYPE: TypeHint = TypeHint::union(&[ + TypeHint::module_attr("typing", "Any"), + TypeHint::builtin("None"), + ]); #[inline] fn extract( @@ -130,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: &'static str = "str"; + const INPUT_TYPE: TypeHint = TypeHint::builtin("str"); #[inline] fn extract( @@ -160,7 +165,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 +180,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..da217f92ced 100644 --- a/src/impl_/introspection.rs +++ b/src/impl_/introspection.rs @@ -1,19 +1,20 @@ 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; } #[repr(C)] diff --git a/src/impl_/pyclass.rs b/src/impl_/pyclass.rs index 3fda206eb5b..3405395fb93 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..894a292611a 100644 --- a/src/inspect/mod.rs +++ b/src/inspect/mod.rs @@ -1,4 +1,276 @@ //! 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). +/// +/// 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::inspect::TypeHint; +/// +/// const T: TypeHint = TypeHint::union(&[TypeHint::builtin("int"), TypeHint::module_attr("b", "B")]); +/// assert_eq!(T.to_string(), "int | b.B"); +/// ``` +#[derive(Clone, Copy)] +pub struct TypeHint { + inner: TypeHintExpr, +} + +#[derive(Clone, Copy)] +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 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 }, + } + } + + /// A type contained in a module like `datetime.time` + /// + /// ``` + /// use pyo3::inspect::TypeHint; + /// + /// const T: TypeHint = TypeHint::module_attr("datetime", "time"); + /// assert_eq!(T.to_string(), "datetime.time"); + /// ``` + pub const fn module_attr(module: &'static str, attr: &'static str) -> Self { + Self { + inner: TypeHintExpr::ModuleAttribute { module, attr }, + } + } + + /// 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 }, + } + } + + /// 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 and return the number of written bytes + /// + /// 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]) -> usize { + let original_len = output.len(); + 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); + 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); + 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_hint_and_move_forward(&elts[i], output); + i += 1; + } + output = 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_hint_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_hint_and_move_forward(&slice[i], output); + i += 1; + } + output = write_slice_and_move_forward(b"]}", output); + } + } + original_len - output.len() + } + + /// 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 += 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; + } + count += slice[i].serialized_len_for_introspection(); + i += 1; + } + count + } + } + } +} + +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(" | ")?; + } + 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(", ")?; + } + 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] { + // TODO: use copy_from_slice with MSRV 1.87+ + 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_hint_and_move_forward<'a>( + value: &TypeHint, + output: &'a mut [u8], +) -> &'a mut [u8] { + let written = value.serialize_for_introspection(output); + output.split_at_mut(written).1 +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_to_string() { + const T: TypeHint = TypeHint::subscript( + &TypeHint::builtin("dict"), + &[ + TypeHint::union(&[TypeHint::builtin("int"), TypeHint::builtin("float")]), + TypeHint::module_attr("datetime", "time"), + ], + ); + assert_eq!(T.to_string(), "dict[int | float, datetime.time]") + } + + #[test] + fn test_serialize_for_introspection() { + const T: TypeHint = TypeHint::subscript( + &TypeHint::builtin("dict"), + &[ + TypeHint::union(&[TypeHint::builtin("int"), TypeHint::builtin("float")]), + TypeHint::module_attr("datetime", "time"), + ], + ); + 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/instance.rs b/src/instance.rs index 3f80c6616f1..60fb09caba8 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -4,6 +4,8 @@ use crate::call::PyCallArgs; use crate::conversion::IntoPyObject; use crate::err::{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}; @@ -2196,7 +2198,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 { @@ -2211,7 +2213,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 f7f8d4dbfd1..e15a9c6b8c0 100644 --- a/src/type_object.rs +++ b/src/type_object.rs @@ -1,6 +1,8 @@ //! Python type object information use crate::ffi_ptr_ext::FfiPtrExt; +#[cfg(feature = "experimental-inspect")] +use crate::inspect::TypeHint; use crate::types::{PyAny, PyType}; use crate::{ffi, Bound, Python}; use std::ptr; @@ -46,9 +48,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 = TypeHint::module_attr("typing", "Any"); /// Returns the PyTypeObject instance for this type. fn type_object_raw(py: Python<'_>) -> *mut ffi::PyTypeObject; @@ -103,9 +105,9 @@ pub unsafe trait PyTypeCheck { )] 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. /// @@ -125,7 +127,7 @@ where const NAME: &'static str = T::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 de81684e035..c5de574db5b 100644 --- a/src/types/boolobject.rs +++ b/src/types/boolobject.rs @@ -1,13 +1,14 @@ +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; +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; @@ -144,7 +145,7 @@ impl<'py> IntoPyObject<'py> for bool { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = "bool"; + const OUTPUT_TYPE: TypeHint = TypeHint::builtin("bool"); #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { @@ -163,7 +164,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 { @@ -183,7 +184,7 @@ impl FromPyObject<'_, '_> for bool { type Error = PyErr; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: &'static str = "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 6fbe2d3679b..5a3e22802aa 100644 --- a/src/types/float.rs +++ b/src/types/float.rs @@ -1,6 +1,8 @@ use crate::conversion::IntoPyObject; #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; +#[cfg(feature = "experimental-inspect")] +use crate::inspect::TypeHint; use crate::{ ffi, ffi_ptr_ext::FfiPtrExt, instance::Bound, Borrowed, FromPyObject, PyAny, PyErr, Python, }; @@ -73,7 +75,7 @@ impl<'py> IntoPyObject<'py> for f64 { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = "float"; + const OUTPUT_TYPE: TypeHint = TypeHint::builtin("float"); #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { @@ -92,7 +94,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 +111,7 @@ impl<'py> FromPyObject<'_, 'py> for f64 { type Error = PyErr; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: &'static str = "float"; + const INPUT_TYPE: TypeHint = TypeHint::builtin("float"); // PyFloat_AsDouble returns -1.0 upon failure #[allow(clippy::float_cmp)] @@ -146,7 +148,7 @@ impl<'py> IntoPyObject<'py> for f32 { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = "float"; + const OUTPUT_TYPE: TypeHint = TypeHint::builtin("float"); #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { @@ -165,7 +167,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 +184,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 = 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 4c5772f98f2..b22e4211869 100644 --- a/src/types/weakref/anyref.rs +++ b/src/types/weakref/anyref.rs @@ -1,5 +1,7 @@ use crate::err::PyResult; use crate::ffi_ptr_ext::FfiPtrExt; +#[cfg(feature = "experimental-inspect")] +use crate::inspect::TypeHint; use crate::sync::PyOnceLock; use crate::type_object::{PyTypeCheck, PyTypeInfo}; use crate::types::any::PyAny; @@ -22,8 +24,10 @@ unsafe impl PyTypeCheck for PyWeakref { const NAME: &'static str = "weakref"; #[cfg(feature = "experimental-inspect")] - const PYTHON_TYPE: &'static str = - "weakref.ProxyType | weakref.CallableProxyType | weakref.ReferenceType"; + const TYPE_HINT: TypeHint = TypeHint::union(&[ + PyWeakrefProxy::TYPE_HINT, + ::TYPE_HINT, + ]); #[inline] fn type_check(object: &Bound<'_, PyAny>) -> bool { diff --git a/src/types/weakref/proxy.rs b/src/types/weakref/proxy.rs index 6f2bc1d57ac..7ccd1810c49 100644 --- a/src/types/weakref/proxy.rs +++ b/src/types/weakref/proxy.rs @@ -1,6 +1,8 @@ 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::sync::PyOnceLock; use crate::type_object::PyTypeCheck; @@ -24,7 +26,10 @@ unsafe 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 = TypeHint::union(&[ + TypeHint::module_attr("weakref", "ProxyType"), + TypeHint::module_attr("weakref", "CallableProxyType"), + ]); #[inline] fn type_check(object: &Bound<'_, PyAny>) -> bool {