From 373964dc3646c2dca26e4f3bb1d2069d06acfb8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Thu, 12 May 2022 14:52:24 +0200 Subject: [PATCH 1/6] Created #[pyo3(type_signature)] --- pyo3-macros-backend/src/attributes.rs | 2 + pyo3-macros-backend/src/pyclass.rs | 22 ++++++++-- pyo3-macros-backend/src/pyfunction.rs | 12 ++++++ tests/test_hints.rs | 61 +++++++++++++++++++++++++++ 4 files changed, 93 insertions(+), 4 deletions(-) create mode 100644 tests/test_hints.rs diff --git a/pyo3-macros-backend/src/attributes.rs b/pyo3-macros-backend/src/attributes.rs index 66e1a4e1891..27aee44c1ea 100644 --- a/pyo3-macros-backend/src/attributes.rs +++ b/pyo3-macros-backend/src/attributes.rs @@ -26,6 +26,7 @@ pub mod kw { syn::custom_keyword!(signature); syn::custom_keyword!(subclass); syn::custom_keyword!(text_signature); + syn::custom_keyword!(type_signature); syn::custom_keyword!(transparent); syn::custom_keyword!(unsendable); syn::custom_keyword!(weakref); @@ -82,6 +83,7 @@ pub type FreelistAttribute = KeywordAttribute>; pub type ModuleAttribute = KeywordAttribute; pub type NameAttribute = KeywordAttribute; pub type TextSignatureAttribute = KeywordAttribute; +pub type TypeSignatureAttribute = KeywordAttribute; impl Parse for KeywordAttribute { fn parse(input: ParseStream<'_>) -> Result { diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index d421acf5d9e..6699a0961b7 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -1,9 +1,6 @@ // Copyright (c) 2017-present PyO3 Project and Contributors -use crate::attributes::{ - self, kw, take_pyo3_options, CrateAttribute, ExtendsAttribute, FreelistAttribute, - ModuleAttribute, NameAttribute, NameLitStr, TextSignatureAttribute, -}; +use crate::attributes::{self, kw, take_pyo3_options, CrateAttribute, ExtendsAttribute, FreelistAttribute, ModuleAttribute, NameAttribute, NameLitStr, TextSignatureAttribute, TypeSignatureAttribute}; use crate::deprecations::{Deprecation, Deprecations}; use crate::konst::{ConstAttributes, ConstSpec}; use crate::pyimpl::{gen_default_items, gen_py_const, PyClassMethodsType}; @@ -60,6 +57,7 @@ pub struct PyClassPyO3Options { pub name: Option, pub subclass: Option, pub text_signature: Option, + pub type_signature: Option, pub unsendable: Option, pub weakref: Option, @@ -77,6 +75,7 @@ enum PyClassPyO3Option { Name(NameAttribute), Subclass(kw::subclass), TextSignature(TextSignatureAttribute), + TypeSignature(TypeSignatureAttribute), Unsendable(kw::unsendable), Weakref(kw::weakref), @@ -106,6 +105,8 @@ impl Parse for PyClassPyO3Option { input.parse().map(PyClassPyO3Option::Subclass) } else if lookahead.peek(attributes::kw::text_signature) { input.parse().map(PyClassPyO3Option::TextSignature) + } else if lookahead.peek(attributes::kw::type_signature) { + input.parse().map(PyClassPyO3Option::TypeSignature) } else if lookahead.peek(attributes::kw::unsendable) { input.parse().map(PyClassPyO3Option::Unsendable) } else if lookahead.peek(attributes::kw::weakref) { @@ -159,6 +160,7 @@ impl PyClassPyO3Options { PyClassPyO3Option::Name(name) => set_option!(name), PyClassPyO3Option::Subclass(subclass) => set_option!(subclass), PyClassPyO3Option::TextSignature(text_signature) => set_option!(text_signature), + PyClassPyO3Option::TypeSignature(type_signature) => set_option!(type_signature), PyClassPyO3Option::Unsendable(unsendable) => set_option!(unsendable), PyClassPyO3Option::Weakref(weakref) => set_option!(weakref), @@ -221,12 +223,14 @@ struct FieldPyO3Options { get: bool, set: bool, name: Option, + type_signature: Option } enum FieldPyO3Option { Get(attributes::kw::get), Set(attributes::kw::set), Name(NameAttribute), + TypeSignature(TypeSignatureAttribute), } impl Parse for FieldPyO3Option { @@ -238,6 +242,8 @@ impl Parse for FieldPyO3Option { input.parse().map(FieldPyO3Option::Set) } else if lookahead.peek(attributes::kw::name) { input.parse().map(FieldPyO3Option::Name) + } else if lookahead.peek(attributes::kw::type_signature) { + input.parse().map(FieldPyO3Option::TypeSignature) } else { Err(lookahead.error()) } @@ -250,6 +256,7 @@ impl FieldPyO3Options { get: false, set: false, name: None, + type_signature: None, }; for option in take_pyo3_options(attrs)? { @@ -275,6 +282,13 @@ impl FieldPyO3Options { ); options.name = Some(name); } + FieldPyO3Option::TypeSignature(typing) => { + ensure_spanned!( + options.type_signature.is_none(), + typing.span() => "`type_signature` may only be specified once" + ); + options.type_signature = Some(typing); + } } } diff --git a/pyo3-macros-backend/src/pyfunction.rs b/pyo3-macros-backend/src/pyfunction.rs index 60ad2afe9ae..df3dcab9e33 100644 --- a/pyo3-macros-backend/src/pyfunction.rs +++ b/pyo3-macros-backend/src/pyfunction.rs @@ -18,6 +18,7 @@ use syn::{ parse::{Parse, ParseBuffer, ParseStream}, token::Comma, }; +use crate::attributes::TypeSignatureAttribute; #[derive(Debug, Clone, PartialEq)] pub enum Argument { @@ -238,6 +239,7 @@ pub struct PyFunctionOptions { pub name: Option, pub signature: Option, pub text_signature: Option, + pub type_signature: Option, pub deprecations: Deprecations, pub krate: Option, } @@ -278,6 +280,7 @@ pub enum PyFunctionOption { PassModule(attributes::kw::pass_module), Signature(PyFunctionSignature), TextSignature(TextSignatureAttribute), + TypeSignature(TypeSignatureAttribute), Crate(CrateAttribute), } @@ -292,6 +295,8 @@ impl Parse for PyFunctionOption { input.parse().map(PyFunctionOption::Signature) } else if lookahead.peek(attributes::kw::text_signature) { input.parse().map(PyFunctionOption::TextSignature) + } else if lookahead.peek(attributes::kw::type_signature) { + input.parse().map(PyFunctionOption::TypeSignature) } else if lookahead.peek(syn::Token![crate]) { input.parse().map(PyFunctionOption::Crate) } else { @@ -336,6 +341,13 @@ impl PyFunctionOptions { ); self.text_signature = Some(text_signature); } + PyFunctionOption::TypeSignature(type_signature) => { + ensure_spanned!( + self.type_signature.is_none(), + type_signature.kw.span() => "`type_signature` may only be specified once" + ); + self.type_signature = Some(type_signature); + } PyFunctionOption::Crate(path) => { ensure_spanned!( self.krate.is_none(), diff --git a/tests/test_hints.rs b/tests/test_hints.rs new file mode 100644 index 00000000000..a384d332bc5 --- /dev/null +++ b/tests/test_hints.rs @@ -0,0 +1,61 @@ +use pyo3::prelude::*; +use pyo3::types::PyType; + +mod common; + +#[pyclass(text_signature = "(value, /)", type_signature = "(int) -> None")] +struct CustomNumber(usize); + +#[pyclass] +struct CustomNumber2 { + #[pyo3(get, set, name = "value", type_signature = "int")] + inner: usize, +} + +#[pyfunction] +#[pyo3(type_signature = "(float) -> CustomNumber")] +fn number_from_float(input: f64) -> CustomNumber { + CustomNumber::from_double(input) +} + +#[pymethods] +impl CustomNumber { + #[new] + fn new(value: usize) -> Self { + Self(value) + } + + #[pyo3(text_signature = "(new, /)", type_signature = "(int) -> None")] + fn set(&mut self, new: usize) { + self.0 = new + } + + #[pyo3(type_signature = "(int) -> CustomNumber")] + fn __add__(&mut self, other: usize) -> Self { + Self(self.0 + other) + } + + #[getter(value)] + #[pyo3(type_signature = "() -> int")] + fn get_value(&self) -> usize { + self.0 + } + + #[setter(value)] + #[pyo3(type_signature = "(int) -> None")] + fn set_value(&mut self, new: usize) { + self.0 = new + } + + #[classmethod] + #[pyo3(text_signature = "(value, /)", type_signature = "(float) -> CustomNumber")] + fn from_float(_cls: &PyType, value: f32) -> Self { + Self(value as usize) + } + + #[staticmethod] + #[pyo3(text_signature = "(value, /)", type_signature = "(float) -> CustomNumber")] + fn from_double(value: f64) -> Self { + Self(value as usize) + } +} From a6d4ea87cb06bd5cdcef243e9d20d05905c600dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Thu, 12 May 2022 16:49:45 +0200 Subject: [PATCH 2/6] Created the 'generate-stubs' feature --- Cargo.toml | 3 +++ pyo3-macros-backend/Cargo.toml | 1 + pyo3-macros/Cargo.toml | 1 + 3 files changed, 5 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 182234f5076..a8d3486a24f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -91,6 +91,9 @@ auto-initialize = [] # Optimizes PyObject to Vec conversion and so on. nightly = [] +# Generates Python stubs (.pyi) files during compilation +generate-stubs = ["pyo3-macros/generate-stubs"] + # Activates all additional features # This is mostly intended for testing purposes - activating *all* of these isn't particularly useful. full = [ diff --git a/pyo3-macros-backend/Cargo.toml b/pyo3-macros-backend/Cargo.toml index b8e543cd094..ddfbddfd00e 100644 --- a/pyo3-macros-backend/Cargo.toml +++ b/pyo3-macros-backend/Cargo.toml @@ -25,3 +25,4 @@ features = ["derive", "parsing", "printing", "clone-impls", "full", "extra-trait [features] pyproto = [] abi3 = [] +generate-stubs = [] diff --git a/pyo3-macros/Cargo.toml b/pyo3-macros/Cargo.toml index 7758fb3bc5a..ddbad0dca0b 100644 --- a/pyo3-macros/Cargo.toml +++ b/pyo3-macros/Cargo.toml @@ -16,6 +16,7 @@ proc-macro = true [features] multiple-pymethods = [] +generate-stubs = ["pyo3-macros-backend/generate-stubs"] pyproto = ["pyo3-macros-backend/pyproto"] abi3 = ["pyo3-macros-backend/abi3"] From 0176d074d33eb28f110f43f4cf19ef20bfe3cdcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Fri, 13 May 2022 10:20:03 +0200 Subject: [PATCH 3/6] Stubs: generate fields and documentation --- pyo3-macros-backend/src/pyclass.rs | 94 +++++++++++++++++++++++++++++- tests/test_hints.rs | 15 +++++ 2 files changed, 108 insertions(+), 1 deletion(-) diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 6699a0961b7..7eac4dbc1d6 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -6,12 +6,13 @@ use crate::konst::{ConstAttributes, ConstSpec}; use crate::pyimpl::{gen_default_items, gen_py_const, PyClassMethodsType}; use crate::pymethod::{impl_py_getter_def, impl_py_setter_def, PropertyType}; use crate::utils::{self, get_pyo3_crate, PythonDoc}; -use proc_macro2::{Span, TokenStream}; +use proc_macro2::{Span, TokenStream, TokenTree}; use quote::quote; use syn::ext::IdentExt; use syn::parse::{Parse, ParseStream}; use syn::punctuated::Punctuated; use syn::{parse_quote, spanned::Spanned, Result, Token}; +use syn::__private::str; /// If the class is derived from a Rust `struct` or `enum`. #[derive(Copy, Clone, Debug, PartialEq, Eq)] @@ -215,6 +216,7 @@ pub fn build_py_class( } }; + generate_stub_class(&class.ident, &args, &doc, &field_options, &methods_type, &krate); impl_class(&class.ident, &args, doc, field_options, methods_type, krate) } @@ -985,3 +987,93 @@ fn define_inventory_class(inventory_class_name: &syn::Ident) -> TokenStream { _pyo3::inventory::collect!(#inventory_class_name); } } + +#[cfg_attr(not(feature = "generate-stubs"), allow(unused_variables))] +fn generate_stub_class( + cls: &syn::Ident, + args: &PyClassArgs, + doc: &PythonDoc, + field_options: &Vec<(&syn::Field, FieldPyO3Options)>, + methods_type: &PyClassMethodsType, + krate: &syn::Path, +) { + #[cfg(feature = "generate-stubs")] { + //TODO: declare inheritance, if required + println!("class {}:", get_class_python_name(cls, args).to_string()); + + let mut is_empty = true; // if the class is empty, it should contain 'pass' + + // Generate the documentation + let doc: TokenStream = quote! {#doc}; + + let mut doc_iter = doc.into_iter(); + { // The first token should be 'concat' + let first_token = doc_iter.next() + .expect("All documentation comments start with a 'concat!' macro, but the documentation is empty"); + match first_token { + TokenTree::Ident(ref ident) => assert_eq!("concat".to_string(), ident.to_string(), "All documentation comments start with a 'concat!' macro, but the first token is not 'concat': {:?}", first_token), + _ => panic!("All documentation comments start with a 'concat!' macro, but the first token is not an identifier: {:?}", first_token) + } + } + { // The second token should be '!' (because it's the concat! macro) + let second_token = doc_iter.next() + .expect("All documentation comments start with a 'concat!' macro, but the documentation only has a single token"); + match second_token { + TokenTree::Punct(ref punct) => assert_eq!('!', punct.as_char(), "All documentation comments start with a 'concat!' macro, but the word 'concat' is not followed by '!': {:?}", punct), + _ => panic!("All documentation comments start with a 'concat!' macro, but the second token is not a '!': {:?}", second_token) + } + } + { // The third item should be a group that contains the actual documentation text + let third_token = doc_iter.next() + .expect("All documentation comments start with a 'concat!' macro, but it has no arguments"); + match third_token { + TokenTree::Group(ref group) => { + println!("\t\"\"\""); + for line in group.stream() { + if let TokenTree::Literal(ref literal) = line { + let unescaped_literal = literal.to_string() + .replace("\\'", "'"); + + let line = unescaped_literal + .trim_start_matches("\"") + .trim_end_matches("\""); + + if line != "\\n" && line != "\\u{0}" { + for sub_line in line.split("\\n") { + println!("\t{}", sub_line); + } + } + } + } + println!("\t\"\"\""); + }, + _ => panic!("All documentation comments start with a 'concat!' macro, but its arguments are not a group: {:?}", third_token) + } + } + + // Generate the different top-level fields + for (field, options) in field_options { + if options.get || options.set { + is_empty = false; + + let name = options.name.as_ref().map(|n| &n.value.0) + .unwrap_or(field.ident.as_ref().expect("This field has neither the #[pyo3(name = ...)] macro applied to, nor a Rust name, it's impossible to create a stub for it")) + .to_string(); + + print!("\t{} = None", name); + + if let Some(typing) = &options.type_signature { + print!(" # type: {}", typing.value.value()) + } + + println!(); + } + } + + //TODO: methods + + if is_empty { + println!("\tpass"); + } + } +} diff --git a/tests/test_hints.rs b/tests/test_hints.rs index a384d332bc5..fb582f59d26 100644 --- a/tests/test_hints.rs +++ b/tests/test_hints.rs @@ -3,15 +3,25 @@ use pyo3::types::PyType; mod common; +// This implementation has a text signature and multiple methods #[pyclass(text_signature = "(value, /)", type_signature = "(int) -> None")] struct CustomNumber(usize); +// This implementation has documentation +/// It's basically just a number. +/// +/// There are multiple documentation lines here. #[pyclass] struct CustomNumber2 { #[pyo3(get, set, name = "value", type_signature = "int")] inner: usize, } +// This implementation is simply empty, with no documentation, methods nor fields +#[pyclass] +struct CustomNumber3(usize); + +/// There's documentation here too. #[pyfunction] #[pyo3(type_signature = "(float) -> CustomNumber")] fn number_from_float(input: f64) -> CustomNumber { @@ -35,6 +45,7 @@ impl CustomNumber { Self(self.0 + other) } + /// This is documented. #[getter(value)] #[pyo3(type_signature = "() -> int")] fn get_value(&self) -> usize { @@ -47,6 +58,10 @@ impl CustomNumber { self.0 = new } + /// Converts a `float` into a `CustomNumber`. + /// + /// :param value: The value we want to convert + /// :return: The result of the conversion #[classmethod] #[pyo3(text_signature = "(value, /)", type_signature = "(float) -> CustomNumber")] fn from_float(_cls: &PyType, value: f32) -> Self { From 8f7c82788f41d0bb405bad4f87bce52847d74e50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Mon, 16 May 2022 10:56:45 +0200 Subject: [PATCH 4/6] Stubs: generate methods --- pyo3-macros-backend/src/lib.rs | 1 + pyo3-macros-backend/src/pyclass.rs | 7 +++-- pyo3-macros-backend/src/pymethod.rs | 45 +++++++++++++++++++++++++++++ pyo3-macros-backend/src/stubs.rs | 38 ++++++++++++++++++++++++ tests/test_hints.rs | 2 +- 5 files changed, 90 insertions(+), 3 deletions(-) create mode 100644 pyo3-macros-backend/src/stubs.rs diff --git a/pyo3-macros-backend/src/lib.rs b/pyo3-macros-backend/src/lib.rs index 7ed8e504f6a..cff4d18e25d 100644 --- a/pyo3-macros-backend/src/lib.rs +++ b/pyo3-macros-backend/src/lib.rs @@ -27,6 +27,7 @@ mod pymethod; #[cfg(feature = "pyproto")] mod pyproto; mod wrap; +mod stubs; pub use frompyobject::build_derive_from_pyobject; pub use module::{process_functions_in_module, pymodule_impl, PyModuleOptions}; diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 7eac4dbc1d6..229d39688e4 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -999,6 +999,7 @@ fn generate_stub_class( ) { #[cfg(feature = "generate-stubs")] { //TODO: declare inheritance, if required + println!("# ID: {}", cls); println!("class {}:", get_class_python_name(cls, args).to_string()); let mut is_empty = true; // if the class is empty, it should contain 'pass' @@ -1060,10 +1061,12 @@ fn generate_stub_class( .unwrap_or(field.ident.as_ref().expect("This field has neither the #[pyo3(name = ...)] macro applied to, nor a Rust name, it's impossible to create a stub for it")) .to_string(); - print!("\t{} = None", name); + print!("\t{}", name); if let Some(typing) = &options.type_signature { - print!(" # type: {}", typing.value.value()) + print!(": {}", typing.value.value()) + } else { + print!(": Any") } println!(); diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index 9d5a1bbf20f..8f14960084c 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -16,6 +16,7 @@ use proc_macro2::{Span, TokenStream}; use quote::{format_ident, quote, ToTokens}; use syn::Ident; use syn::{ext::IdentExt, spanned::Spanned, Result}; +use crate::stubs::map_rust_type_to_python; pub enum GeneratedPyMethod { Method(TokenStream), @@ -176,6 +177,8 @@ pub fn gen_py_method( let method = PyMethod::parse(sig, &mut *meth_attrs, options)?; let spec = &method.spec; + generate_stub_method(cls, spec); + Ok(match (method.kind, &spec.tp) { // Class attributes go before protos so that class attributes can be used to set proto // method to None. @@ -230,6 +233,48 @@ pub fn gen_py_method( }) } +#[cfg_attr(not(feature = "generate-stubs"), allow(unused_variables))] +fn generate_stub_method(cls: &syn::Type, method: &FnSpec) { + #[cfg(feature = "generate-stubs")] { + println!("# ID: {}", cls.to_token_stream()); + + match &method.tp { + FnType::FnStatic => println!("\t@staticmethod"), + FnType::FnClass => println!("\t@classmethod"), + FnType::Getter(_) => println!("\t@property"), + FnType::Setter(_) => println!("\t@{}.setter", method.python_name), + _ => {}, + } + + print!("\tdef {}(", method.python_name); + + let mut comma_required = false; + match &method.tp { + FnType::Fn(_) | FnType::Getter(_) | FnType::Setter(_) => { + comma_required = true; + print!("self") + }, + FnType::FnClass | FnType::FnNew => { + comma_required = true; + print!("cls") + }, + _ => {} + } + + for arg in &method.args { + if comma_required { + print!(", "); + } + + print!("{}: {}", arg.name, map_rust_type_to_python(&arg.ty)); + + comma_required = true; + } + println!(") -> {}: ...", map_rust_type_to_python(&method.output)); + + } +} + pub fn check_generic(sig: &syn::Signature) -> syn::Result<()> { let err_msg = |typ| format!("Python functions cannot have generic {} parameters", typ); for param in &sig.generics.params { diff --git a/pyo3-macros-backend/src/stubs.rs b/pyo3-macros-backend/src/stubs.rs new file mode 100644 index 00000000000..58c1cc67a87 --- /dev/null +++ b/pyo3-macros-backend/src/stubs.rs @@ -0,0 +1,38 @@ +use syn::Type; + +pub fn map_rust_type_to_python(rust: &Type) -> String { + let rust_str = match rust { + Type::Array(_) => todo!("Unknown how to map to Python: {:?}", rust), + Type::BareFn(_) => todo!("Unknown how to map to Python: {:?}", rust), + Type::Group(_) => todo!("Unknown how to map to Python: {:?}", rust), + Type::ImplTrait(_) => todo!("Unknown how to map to Python: {:?}", rust), + Type::Infer(_) => "None".to_string(), + Type::Macro(_) => todo!("Unknown how to map to Python: {:?}", rust), + Type::Never(_) => todo!("Unknown how to map to Python: {:?}", rust), + Type::Paren(_) => todo!("Unknown how to map to Python: {:?}", rust), + Type::Path(ref path) => { + match &path.path.segments.last() { + Some(ref segment) => segment.ident.to_string(), + _ => "Any".to_string() + } + }, + Type::Reference(_) => todo!("Unknown how to map to Python: {:?}", rust), + Type::Slice(_) => todo!("Unknown how to map to Python: {:?}", rust), + Type::TraitObject(_) => todo!("Unknown how to map to Python: {:?}", rust), + Type::Tuple(_) => todo!("Unknown how to map to Python: {:?}", rust), + Type::Verbatim(_) => todo!("Unknown how to map to Python: {:?}", rust), + + #[cfg_attr(test, deny(non_exhaustive_omitted_patterns))] + _ => { + "Any".to_string() + } + }; + + match rust_str.as_str() { + "usize" | "isize" | "u32" | "u64" | "i32" | "i64" => "int".to_string(), + "f64" | "f32" => "float".to_string(), + "bool" => "bool".to_string(), + "None" | "Any" => rust_str, + _ => format!("rust_{}", rust_str) + } +} diff --git a/tests/test_hints.rs b/tests/test_hints.rs index fb582f59d26..006031d9927 100644 --- a/tests/test_hints.rs +++ b/tests/test_hints.rs @@ -4,7 +4,7 @@ use pyo3::types::PyType; mod common; // This implementation has a text signature and multiple methods -#[pyclass(text_signature = "(value, /)", type_signature = "(int) -> None")] +#[pyclass(name = "RustNumber", text_signature = "(value, /)", type_signature = "(int) -> None")] struct CustomNumber(usize); // This implementation has documentation From 630d9777c3c596338caef16f6d3a1add8b2c7709 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Mon, 16 May 2022 11:36:53 +0200 Subject: [PATCH 5/6] Stubs: parameters with a default value --- pyo3-macros-backend/src/pymethod.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index 8f14960084c..43ff58a4551 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -268,6 +268,10 @@ fn generate_stub_method(cls: &syn::Type, method: &FnSpec) { print!("{}: {}", arg.name, map_rust_type_to_python(&arg.ty)); + if arg.optional.is_some() { + print!(" = ..."); + } + comma_required = true; } println!(") -> {}: ...", map_rust_type_to_python(&method.output)); From 6226bf9357e1a1ca6a066f065eb163bd09eb3c16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Mon, 16 May 2022 11:37:41 +0200 Subject: [PATCH 6/6] Stubs: Vec and Option --- pyo3-macros-backend/src/stubs.rs | 47 ++++++++++++++++++++++++++------ tests/test_hints.rs | 9 ++++++ 2 files changed, 47 insertions(+), 9 deletions(-) diff --git a/pyo3-macros-backend/src/stubs.rs b/pyo3-macros-backend/src/stubs.rs index 58c1cc67a87..c4a669fe247 100644 --- a/pyo3-macros-backend/src/stubs.rs +++ b/pyo3-macros-backend/src/stubs.rs @@ -1,4 +1,4 @@ -use syn::Type; +use syn::{GenericArgument, PathArguments, Type}; pub fn map_rust_type_to_python(rust: &Type) -> String { let rust_str = match rust { @@ -12,7 +12,42 @@ pub fn map_rust_type_to_python(rust: &Type) -> String { Type::Paren(_) => todo!("Unknown how to map to Python: {:?}", rust), Type::Path(ref path) => { match &path.path.segments.last() { - Some(ref segment) => segment.ident.to_string(), + Some(ref segment) => { + let str = segment.ident.to_string(); + + match str.as_str() { + "Vec" => { + if let PathArguments::AngleBracketed(angle) = &segment.arguments { + let mut res = None; + for arg in &angle.args { + if let GenericArgument::Type(tp) = arg { + res = Some(format!("List[{}]", map_rust_type_to_python(tp))) + } + } + res.unwrap_or("List[Any]".to_string()) + } else { + "List[Any]".to_string() + } + } + "Option" => { + if let PathArguments::AngleBracketed(angle) = &segment.arguments { + let mut res = None; + for arg in &angle.args { + if let GenericArgument::Type(tp) = arg { + res = Some(format!("Optional[{}]", map_rust_type_to_python(tp))) + } + } + res.unwrap_or("Optional[Any]".to_string()) + } else { + "Optional[Any]".to_string() + } + } + "usize" | "isize" | "u32" | "u64" | "i32" | "i64" => "int".to_string(), + "f64" | "f32" => "float".to_string(), + "bool" => "bool".to_string(), + _ => format!("rust_{}", str), + } + }, _ => "Any".to_string() } }, @@ -28,11 +63,5 @@ pub fn map_rust_type_to_python(rust: &Type) -> String { } }; - match rust_str.as_str() { - "usize" | "isize" | "u32" | "u64" | "i32" | "i64" => "int".to_string(), - "f64" | "f32" => "float".to_string(), - "bool" => "bool".to_string(), - "None" | "Any" => rust_str, - _ => format!("rust_{}", rust_str) - } + rust_str } diff --git a/tests/test_hints.rs b/tests/test_hints.rs index 006031d9927..c88a59189b0 100644 --- a/tests/test_hints.rs +++ b/tests/test_hints.rs @@ -73,4 +73,13 @@ impl CustomNumber { fn from_double(value: f64) -> Self { Self(value as usize) } + + fn to_3(&self) -> CustomNumber3 { + CustomNumber3(self.0) + } + + #[args(n = "None")] + fn next(&self, n: Option) -> Vec { + todo!() + } }