From cec4c2d2e96feec8fc443d143f58afad313fd9d1 Mon Sep 17 00:00:00 2001 From: David Hewitt <1939362+davidhewitt@users.noreply.github.com> Date: Sat, 5 Jun 2021 16:28:31 +0100 Subject: [PATCH] text_signature: move to `#[pyo3(text_signature = "...")]` --- CHANGELOG.md | 2 + guide/src/building_and_distribution.md | 6 +- guide/src/function.md | 14 ++-- pyo3-macros-backend/src/attributes.rs | 64 ++++++++++++++++++ pyo3-macros-backend/src/deprecations.rs | 2 + pyo3-macros-backend/src/method.rs | 58 ++++++++-------- pyo3-macros-backend/src/module.rs | 3 +- pyo3-macros-backend/src/pyclass.rs | 83 +++++++++++++++++++++-- pyo3-macros-backend/src/pyfunction.rs | 34 ++++++++-- pyo3-macros-backend/src/pymethod.rs | 2 +- pyo3-macros-backend/src/utils.rs | 90 +++++-------------------- pyo3-macros/src/lib.rs | 7 +- src/derive_utils.rs | 2 +- src/impl_/deprecations.rs | 6 ++ tests/test_text_signature.rs | 20 +++--- tests/ui/deprecations.rs | 5 ++ tests/ui/deprecations.stderr | 50 +++++++++++--- tests/ui/invalid_pymethods.rs | 10 +-- tests/ui/invalid_pymethods.stderr | 30 ++++----- 19 files changed, 317 insertions(+), 171 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f871c26e603..a16425788c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Add FFI definition `PyDateTime_TimeZone_UTC`. [#1572](https://github.com/PyO3/pyo3/pull/1572) - Add support for `#[pyclass(extends=Exception)]`. [#1591](https://github.com/PyO3/pyo3/pull/1591) - Add support for extracting `PathBuf` from `pathlib.Path`. [#1654](https://github.com/PyO3/pyo3/pull/1654) +- Add `#[pyo3(text_signature = "...")]` syntax for setting text signature. [#1658](https://github.com/PyO3/pyo3/pull/1658) ### Changed - Allow only one `#[pymethods]` block per `#[pyclass]` by default, to simplify the proc macro implementations. Add `multiple-pymethods` feature to opt-in to the more complex full behavior. [#1457](https://github.com/PyO3/pyo3/pull/1457) @@ -49,6 +50,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Deprecate string-literal second argument to `#[pyfn(m, "name")]`. [#1610](https://github.com/PyO3/pyo3/pull/1610) - No longer call `PyEval_InitThreads()` in `#[pymodule]` init code. [#1630](https://github.com/PyO3/pyo3/pull/1630) - Use `METH_FASTCALL` argument passing convention, when possible, to improve `#[pyfunction]` performance. [#1619](https://github.com/PyO3/pyo3/pull/1619) +- Deprecate `#[text_signature = "..."]` attributes in favor of `#[pyo3(text_signature = "...")]`. [#1658](https://github.com/PyO3/pyo3/pull/1658) ### Removed - Remove deprecated exception names `BaseException` etc. [#1426](https://github.com/PyO3/pyo3/pull/1426) diff --git a/guide/src/building_and_distribution.md b/guide/src/building_and_distribution.md index 8351bb2bb7f..54b5cd34079 100644 --- a/guide/src/building_and_distribution.md +++ b/guide/src/building_and_distribution.md @@ -41,10 +41,10 @@ There are two main ways to test, build and distribute your module as a Python pa ### Manual builds -You can also symlink (or copy) and rename the shared library from the `target` folder: +You can also symlink (or copy) and rename the shared library from the `target` folder: - on macOS, rename `libyour_module.dylib` to `your_module.so`. - on Windows, rename `libyour_module.dll` to `your_module.pyd`. -- on Linux, rename `libyour_module.so` to `your_module.so`. +- on Linux, rename `libyour_module.so` to `your_module.so`. You can then open a Python shell in the same folder and you'll be able to use `import your_module`. @@ -86,7 +86,7 @@ As an advanced feature, you can build PyO3 wheel without calling Python interpre Due to limitations in the Python API, there are a few `pyo3` features that do not work when compiling for `abi3`. These are: -- `#[text_signature]` does not work on classes until Python 3.10 or greater. +- `#[pyo3(text_signature = "...")]` does not work on classes until Python 3.10 or greater. - The `dict` and `weakref` options on classes are not supported until Python 3.9 or greater. - The buffer API is not supported. - Optimizations which rely on knowledge of the exact Python version compiled against. diff --git a/guide/src/function.md b/guide/src/function.md index 0517e49e60d..f2a2d1b41ee 100644 --- a/guide/src/function.md +++ b/guide/src/function.md @@ -117,7 +117,7 @@ fn module_with_functions(py: Python, m: &PyModule) -> PyResult<()> { ## Making the function signature available to Python In order to make the function signature available to Python to be retrieved via -`inspect.signature`, use the `#[text_signature]` annotation as in the example +`inspect.signature`, use the `#[pyo3(text_signature)]` annotation as in the example below. The `/` signifies the end of positional-only arguments. (This is not a feature of this library in particular, but the general format used by CPython for annotating signatures of built-in functions.) @@ -127,7 +127,7 @@ use pyo3::prelude::*; /// This function adds two unsigned 64-bit integers. #[pyfunction] -#[text_signature = "(a, b, /)"] +#[pyo3(text_signature = "(a, b, /)")] fn add(a: u64, b: u64) -> u64 { a + b } @@ -142,7 +142,7 @@ use pyo3::types::PyType; // it works even if the item is not documented: #[pyclass] -#[text_signature = "(c, d, /)"] +#[pyo3(text_signature = "(c, d, /)")] struct MyClass {} #[pymethods] @@ -154,17 +154,17 @@ impl MyClass { Self {} } // the self argument should be written $self - #[text_signature = "($self, e, f)"] + #[pyo3(text_signature = "($self, e, f)")] fn my_method(&self, e: i32, f: i32) -> i32 { e + f } #[classmethod] - #[text_signature = "(cls, e, f)"] + #[pyo3(text_signature = "(cls, e, f)")] fn my_class_method(cls: &PyType, e: i32, f: i32) -> i32 { e + f } #[staticmethod] - #[text_signature = "(e, f)"] + #[pyo3(text_signature = "(e, f)")] fn my_static_method(e: i32, f: i32) -> i32 { e + f } @@ -180,7 +180,7 @@ Alternatively, simply make sure the first line of your docstring is formatted like in the following example. Please note that the newline after the `--` is mandatory. The `/` signifies the end of positional-only arguments. -`#[text_signature]` should be preferred, since it will override automatically +`#[pyo3(text_signature)]` should be preferred, since it will override automatically generated signatures when those are added in a future version of PyO3. ```rust diff --git a/pyo3-macros-backend/src/attributes.rs b/pyo3-macros-backend/src/attributes.rs index d10f2f52602..04dcc054859 100644 --- a/pyo3-macros-backend/src/attributes.rs +++ b/pyo3-macros-backend/src/attributes.rs @@ -18,6 +18,7 @@ pub mod kw { syn::custom_keyword!(name); syn::custom_keyword!(set); syn::custom_keyword!(signature); + syn::custom_keyword!(text_signature); syn::custom_keyword!(transparent); } @@ -45,6 +46,23 @@ impl Parse for NameAttribute { } } +#[derive(Clone, Debug, PartialEq)] +pub struct TextSignatureAttribute { + pub kw: kw::text_signature, + pub eq_token: Token![=], + pub lit: LitStr, +} + +impl Parse for TextSignatureAttribute { + fn parse(input: ParseStream) -> Result { + Ok(TextSignatureAttribute { + kw: input.parse()?, + eq_token: input.parse()?, + lit: input.parse()?, + }) + } +} + pub fn get_pyo3_options(attr: &syn::Attribute) -> Result>> { if is_attribute_ident(attr, "pyo3") { attr.parse_args_with(Punctuated::parse_terminated).map(Some) @@ -112,3 +130,49 @@ pub fn get_deprecated_name_attribute( _ => Ok(None), } } + +pub fn get_deprecated_text_signature_attribute( + attr: &syn::Attribute, + deprecations: &mut Deprecations, +) -> syn::Result> { + match attr.parse_meta() { + Ok(syn::Meta::NameValue(syn::MetaNameValue { + path, + lit: syn::Lit::Str(lit), + .. + })) if path.is_ident("text_signature") => { + let text_signature = TextSignatureAttribute { + kw: syn::parse_quote!(text_signature), + eq_token: syn::parse_quote!(=), + lit, + }; + deprecations.push( + crate::deprecations::Deprecation::TextSignatureAttribute, + attr.span(), + ); + Ok(Some(text_signature)) + } + _ => Ok(None), + } +} + +pub fn take_deprecated_text_signature_attribute( + attrs: &mut Vec, + deprecations: &mut Deprecations, +) -> syn::Result> { + let mut text_signature = None; + let mut attrs_out = Vec::with_capacity(attrs.len()); + for attr in attrs.drain(..) { + if let Some(value) = get_deprecated_text_signature_attribute(&attr, deprecations)? { + ensure_spanned!( + text_signature.is_none(), + attr.span() => "text_signature attribute already specified previously" + ); + text_signature = Some(value); + } else { + attrs_out.push(attr); + } + } + *attrs = attrs_out; + Ok(text_signature) +} diff --git a/pyo3-macros-backend/src/deprecations.rs b/pyo3-macros-backend/src/deprecations.rs index 44c100df324..d365d1767ff 100644 --- a/pyo3-macros-backend/src/deprecations.rs +++ b/pyo3-macros-backend/src/deprecations.rs @@ -4,6 +4,7 @@ use quote::{quote_spanned, ToTokens}; pub enum Deprecation { NameAttribute, PyfnNameArgument, + TextSignatureAttribute, } impl Deprecation { @@ -11,6 +12,7 @@ impl Deprecation { let string = match self { Deprecation::NameAttribute => "NAME_ATTRIBUTE", Deprecation::PyfnNameArgument => "PYFN_NAME_ARGUMENT", + Deprecation::TextSignatureAttribute => "TEXT_SIGNATURE_ATTRIBUTE", }; syn::Ident::new(string, span) } diff --git a/pyo3-macros-backend/src/method.rs b/pyo3-macros-backend/src/method.rs index 1e5d4b9b87d..0e9c34bef9e 100644 --- a/pyo3-macros-backend/src/method.rs +++ b/pyo3-macros-backend/src/method.rs @@ -1,5 +1,6 @@ // Copyright (c) 2017-present PyO3 Project and Contributors +use crate::attributes::TextSignatureAttribute; use crate::pyfunction::PyFunctionOptions; use crate::pyfunction::{PyFunctionArgPyO3Attributes, PyFunctionSignature}; use crate::utils; @@ -209,13 +210,19 @@ impl<'a> FnSpec<'a> { } let (fn_type, skip_first_arg) = Self::parse_fn_type(sig, fn_type_attr, &mut python_name)?; + Self::ensure_text_signature_on_valid_method(&fn_type, options.text_signature.as_ref())?; let name = &sig.ident; let ty = get_return_info(&sig.output); let python_name = python_name.as_ref().unwrap_or(name).unraw(); - let text_signature = Self::parse_text_signature(meth_attrs, &fn_type, &python_name)?; - let doc = utils::get_doc(&meth_attrs, text_signature, true)?; + let doc = utils::get_doc( + &meth_attrs, + options + .text_signature + .as_ref() + .map(|attr| (&python_name, attr)), + )?; let arguments = if skip_first_arg { sig.inputs @@ -246,36 +253,27 @@ impl<'a> FnSpec<'a> { syn::LitStr::new(&format!("{}\0", self.python_name), self.python_name.span()) } - fn parse_text_signature( - meth_attrs: &mut Vec, + fn ensure_text_signature_on_valid_method( fn_type: &FnType, - python_name: &syn::Ident, - ) -> syn::Result> { - let mut parse_erroneous_text_signature = |error_msg: &str| { - // try to parse anyway to give better error messages - if let Some(text_signature) = - utils::parse_text_signature_attrs(meth_attrs, &python_name)? - { - bail_spanned!(text_signature.span() => error_msg) - } else { - Ok(None) - } - }; - - let text_signature = match &fn_type { - FnType::Fn(_) | FnType::FnClass | FnType::FnStatic => { - utils::parse_text_signature_attrs(&mut *meth_attrs, &python_name)? + text_signature: Option<&TextSignatureAttribute>, + ) -> syn::Result<()> { + if let Some(text_signature) = text_signature { + match &fn_type { + FnType::FnNew => bail_spanned!( + text_signature.kw.span() => + "text_signature not allowed on __new__; if you want to add a signature on \ + __new__, put it on the struct definition instead" + ), + FnType::FnCall(_) + | FnType::Getter(_) + | FnType::Setter(_) + | FnType::ClassAttribute => bail_spanned!( + text_signature.kw.span() => "text_signature not allowed with this method type" + ), + _ => {} } - FnType::FnNew => parse_erroneous_text_signature( - "text_signature not allowed on __new__; if you want to add a signature on \ - __new__, put it on the struct definition instead", - )?, - FnType::FnCall(_) | FnType::Getter(_) | FnType::Setter(_) | FnType::ClassAttribute => { - parse_erroneous_text_signature("text_signature not allowed with this method type")? - } - }; - - Ok(text_signature) + } + Ok(()) } fn parse_fn_type( diff --git a/pyo3-macros-backend/src/module.rs b/pyo3-macros-backend/src/module.rs index 04f6694d3eb..e06bbed968c 100644 --- a/pyo3-macros-backend/src/module.rs +++ b/pyo3-macros-backend/src/module.rs @@ -14,6 +14,7 @@ use syn::{parse::Parse, spanned::Spanned, token::Comma, Ident, Path}; /// module pub fn py_init(fnname: &Ident, name: &Ident, doc: syn::LitStr) -> TokenStream { let cb_name = Ident::new(&format!("PyInit_{}", name), Span::call_site()); + assert!(doc.value().ends_with('\0')); quote! { #[no_mangle] @@ -23,7 +24,7 @@ pub fn py_init(fnname: &Ident, name: &Ident, doc: syn::LitStr) -> TokenStream { pub unsafe extern "C" fn #cb_name() -> *mut pyo3::ffi::PyObject { use pyo3::derive_utils::ModuleDef; static NAME: &str = concat!(stringify!(#name), "\0"); - static DOC: &str = concat!(#doc, "\0"); + static DOC: &str = #doc; static MODULE_DEF: ModuleDef = unsafe { ModuleDef::new(NAME, DOC) }; pyo3::callback::handle_panic(|_py| { MODULE_DEF.make_module(_py, #fnname) }) diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index e6a979eb881..ff3240238a4 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -1,6 +1,10 @@ // Copyright (c) 2017-present PyO3 Project and Contributors -use crate::attributes::{self, take_pyo3_options, NameAttribute}; +use crate::attributes::{ + self, take_deprecated_text_signature_attribute, take_pyo3_options, NameAttribute, + TextSignatureAttribute, +}; +use crate::deprecations::Deprecations; use crate::pyimpl::PyClassMethodsType; use crate::pymethod::{impl_py_getter_def, impl_py_setter_def, PropertyType}; use crate::utils; @@ -162,16 +166,71 @@ impl PyClassArgs { } } +#[derive(Default)] +pub struct PyClassPyO3Options { + pub text_signature: Option, + pub deprecations: Deprecations, +} + +enum PyClassPyO3Option { + TextSignature(TextSignatureAttribute), +} + +impl Parse for PyClassPyO3Option { + fn parse(input: ParseStream) -> Result { + let lookahead = input.lookahead1(); + if lookahead.peek(attributes::kw::text_signature) { + input.parse().map(PyClassPyO3Option::TextSignature) + } else { + Err(lookahead.error()) + } + } +} + +impl PyClassPyO3Options { + pub fn take_pyo3_options(attrs: &mut Vec) -> syn::Result { + let mut options: PyClassPyO3Options = Default::default(); + for option in take_pyo3_options(attrs)? { + match option { + PyClassPyO3Option::TextSignature(text_signature) => { + options.set_text_signature(text_signature)?; + } + } + } + Ok(options) + } + + pub fn set_text_signature( + &mut self, + text_signature: TextSignatureAttribute, + ) -> syn::Result<()> { + ensure_spanned!( + self.text_signature.is_none(), + text_signature.kw.span() => "`text_signature` may only be specified once" + ); + self.text_signature = Some(text_signature); + Ok(()) + } +} + pub fn build_py_class( class: &mut syn::ItemStruct, - attr: &PyClassArgs, + args: &PyClassArgs, methods_type: PyClassMethodsType, ) -> syn::Result { - let text_signature = utils::parse_text_signature_attrs( - &mut class.attrs, - &get_class_python_name(&class.ident, attr), + let mut options = PyClassPyO3Options::take_pyo3_options(&mut class.attrs)?; + if let Some(text_signature) = + take_deprecated_text_signature_attribute(&mut class.attrs, &mut options.deprecations)? + { + options.set_text_signature(text_signature)?; + } + let doc = utils::get_doc( + &class.attrs, + options + .text_signature + .as_ref() + .map(|attr| (get_class_python_name(&class.ident, args), attr)), )?; - let doc = utils::get_doc(&class.attrs, text_signature, true)?; ensure_spanned!( class.generics.params.is_empty(), @@ -201,7 +260,14 @@ pub fn build_py_class( } }; - impl_class(&class.ident, &attr, doc, field_options, methods_type) + impl_class( + &class.ident, + &args, + doc, + field_options, + methods_type, + options.deprecations, + ) } /// `#[pyo3()]` options for pyclass fields @@ -308,6 +374,7 @@ fn impl_class( doc: syn::LitStr, field_options: Vec<(&syn::Field, FieldPyO3Options)>, methods_type: PyClassMethodsType, + deprecations: Deprecations, ) -> syn::Result { let cls_name = get_class_python_name(cls, attr).to_string(); @@ -429,6 +496,8 @@ fn impl_class( #[inline] fn type_object_raw(py: pyo3::Python) -> *mut pyo3::ffi::PyTypeObject { + #deprecations + use pyo3::type_object::LazyStaticType; static TYPE_OBJECT: LazyStaticType = LazyStaticType::new(); TYPE_OBJECT.get_or_init::(py) diff --git a/pyo3-macros-backend/src/pyfunction.rs b/pyo3-macros-backend/src/pyfunction.rs index 84f25f5bdac..d125d66193f 100644 --- a/pyo3-macros-backend/src/pyfunction.rs +++ b/pyo3-macros-backend/src/pyfunction.rs @@ -2,8 +2,9 @@ use crate::{ attributes::{ - self, get_deprecated_name_attribute, get_pyo3_options, take_attributes, - FromPyWithAttribute, NameAttribute, + self, get_deprecated_name_attribute, get_deprecated_text_signature_attribute, + get_pyo3_options, take_attributes, FromPyWithAttribute, NameAttribute, + TextSignatureAttribute, }, deprecations::Deprecations, method::{self, FnArg, FnSpec}, @@ -211,6 +212,7 @@ pub struct PyFunctionOptions { pub pass_module: bool, pub name: Option, pub signature: Option, + pub text_signature: Option, pub deprecations: Deprecations, } @@ -220,6 +222,7 @@ impl Parse for PyFunctionOptions { pass_module: false, name: None, signature: None, + text_signature: None, deprecations: Deprecations::new(), }; @@ -228,6 +231,7 @@ impl Parse for PyFunctionOptions { if lookahead.peek(attributes::kw::name) || lookahead.peek(attributes::kw::pass_module) || lookahead.peek(attributes::kw::signature) + || lookahead.peek(attributes::kw::text_signature) { options.add_attributes(std::iter::once(input.parse()?))?; if !input.is_empty() { @@ -250,6 +254,7 @@ pub enum PyFunctionOption { Name(NameAttribute), PassModule(attributes::kw::pass_module), Signature(PyFunctionSignature), + TextSignature(TextSignatureAttribute), } impl Parse for PyFunctionOption { @@ -261,6 +266,8 @@ impl Parse for PyFunctionOption { input.parse().map(PyFunctionOption::PassModule) } else if lookahead.peek(attributes::kw::signature) { input.parse().map(PyFunctionOption::Signature) + } else if lookahead.peek(attributes::kw::text_signature) { + input.parse().map(PyFunctionOption::TextSignature) } else { Err(lookahead.error()) } @@ -283,6 +290,13 @@ impl PyFunctionOptions { { self.set_name(name)?; Ok(true) + } else if let Some(text_signature) = + get_deprecated_text_signature_attribute(attr, &mut self.deprecations)? + { + self.add_attributes(std::iter::once(PyFunctionOption::TextSignature( + text_signature, + )))?; + Ok(true) } else { Ok(false) } @@ -313,6 +327,13 @@ impl PyFunctionOptions { ); self.signature = Some(signature); } + PyFunctionOption::TextSignature(text_signature) => { + ensure_spanned!( + self.text_signature.is_none(), + text_signature.kw.span() => "`text_signature` may only be specified once" + ); + self.text_signature = Some(text_signature); + } } } Ok(()) @@ -379,8 +400,13 @@ pub fn impl_wrap_pyfunction( let ty = method::get_return_info(&func.sig.output); - let text_signature = utils::parse_text_signature_attrs(&mut func.attrs, &python_name)?; - let doc = utils::get_doc(&func.attrs, text_signature, true)?; + let doc = utils::get_doc( + &func.attrs, + options + .text_signature + .as_ref() + .map(|attr| (&python_name, attr)), + )?; let function_wrapper_ident = function_wrapper_ident(&func.sig.ident); diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index 9752747ab1c..02da1c8d142 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -876,7 +876,7 @@ impl PropertyType<'_> { fn doc(&self) -> Cow { match self { PropertyType::Descriptor { field, .. } => { - let doc = utils::get_doc(&field.attrs, None, true) + let doc = utils::get_doc(&field.attrs, None) .unwrap_or_else(|_| syn::LitStr::new("", Span::call_site())); Cow::Owned(doc) } diff --git a/pyo3-macros-backend/src/utils.rs b/pyo3-macros-backend/src/utils.rs index 7242e96bcab..b92ef586f31 100644 --- a/pyo3-macros-backend/src/utils.rs +++ b/pyo3-macros-backend/src/utils.rs @@ -2,6 +2,8 @@ use proc_macro2::Span; use syn::spanned::Spanned; +use crate::attributes::TextSignatureAttribute; + /// Macro inspired by `anyhow::anyhow!` to create a compiler error with the given span. macro_rules! err_spanned { ($span:expr => $msg:expr) => { @@ -56,79 +58,19 @@ pub fn option_type_argument(ty: &syn::Type) -> Option<&syn::Type> { None } -pub fn is_text_signature_attr(attr: &syn::Attribute) -> bool { - attr.path.is_ident("text_signature") -} - -fn parse_text_signature_attr( - attr: &syn::Attribute, - python_name: &syn::Ident, -) -> syn::Result> { - if !is_text_signature_attr(attr) { - return Ok(None); - } - let python_name_str = python_name.to_string(); - let python_name_str = python_name_str - .rsplit('.') - .next() - .map(str::trim) - .filter(|v| !v.is_empty()) - .ok_or_else(|| err_spanned!(python_name.span() => "failed to parse python name"))?; - match attr.parse_meta()? { - syn::Meta::NameValue(syn::MetaNameValue { - lit: syn::Lit::Str(lit), - .. - }) => { - let value = lit.value(); - ensure_spanned!( - value.starts_with('(') && value.ends_with(')'), - lit.span() => "text_signature must start with \"(\" and end with \")\"" - ); - Ok(Some(syn::LitStr::new( - &(python_name_str.to_owned() + &value), - lit.span(), - ))) - } - meta => bail_spanned!( - meta.span() => "text_signature must be of the form #[text_signature = \"\"]" - ), - } -} - -pub fn parse_text_signature_attrs( - attrs: &mut Vec, - python_name: &syn::Ident, -) -> syn::Result> { - let mut text_signature = None; - let mut attrs_out = Vec::with_capacity(attrs.len()); - for attr in attrs.drain(..) { - if let Some(value) = parse_text_signature_attr(&attr, python_name)? { - ensure_spanned!( - text_signature.is_none(), - attr.span() => "text_signature attribute already specified previously" - ); - text_signature = Some(value); - } else { - attrs_out.push(attr); - } - } - *attrs = attrs_out; - Ok(text_signature) -} - -// FIXME(althonos): not sure the docstring formatting is on par here. +// Returns a null-terminated syn::LitStr for use as a Python docstring. pub fn get_doc( attrs: &[syn::Attribute], - text_signature: Option, - null_terminated: bool, + text_signature: Option<(&syn::Ident, &TextSignatureAttribute)>, ) -> syn::Result { let mut doc = String::new(); let mut span = Span::call_site(); - if let Some(text_signature) = text_signature { + if let Some((python_name, text_signature)) = text_signature { // create special doc string lines to set `__text_signature__` - span = text_signature.span(); - doc.push_str(&text_signature.value()); + doc.push_str(&python_name.to_string()); + span = text_signature.lit.span(); + doc.push_str(&text_signature.lit.value()); doc.push_str("\n--\n\n"); } @@ -136,9 +78,12 @@ pub fn get_doc( let mut first = true; for attr in attrs.iter() { - if let Ok(syn::Meta::NameValue(metanv)) = attr.parse_meta() { - if metanv.path.is_ident("doc") { - if let syn::Lit::Str(litstr) = metanv.lit { + if attr.path.is_ident("doc") { + match attr.parse_meta()? { + syn::Meta::NameValue(syn::MetaNameValue { + lit: syn::Lit::Str(litstr), + .. + }) => { if first { first = false; span = litstr.span(); @@ -151,16 +96,13 @@ pub fn get_doc( doc.push_str(&d); }; separator = "\n"; - } else { - bail_spanned!(metanv.span() => "invalid doc comment") } + _ => bail_spanned!(attr.span() => "invalid doc comment"), } } } - if null_terminated { - doc.push('\0'); - } + doc.push('\0'); Ok(syn::LitStr::new(&doc, span)) } diff --git a/pyo3-macros/src/lib.rs b/pyo3-macros/src/lib.rs index b46e2091e01..963e1814551 100644 --- a/pyo3-macros/src/lib.rs +++ b/pyo3-macros/src/lib.rs @@ -32,7 +32,7 @@ pub fn pymodule(attr: TokenStream, input: TokenStream) -> TokenStream { return err.to_compile_error().into(); } - let doc = match get_doc(&ast.attrs, None, false) { + let doc = match get_doc(&ast.attrs, None) { Ok(doc) => doc, Err(err) => return err.to_compile_error().into(), }; @@ -185,7 +185,7 @@ pub fn pymethods(_: TokenStream, input: TokenStream) -> TokenStream { /// | [`#[classattr]`][9] | Defines a class variable. | /// | [`#[args]`][10] | Define a method's default arguments and allows the function to receive `*args` and `**kwargs`. | /// -/// Methods within a `#[pymethods]` block can also be annotated with any of the attributes which can +/// Methods within a `#[pymethods]` block can also be annotated with any of the `#[pyo3]` options which can /// be used with [`#[pyfunction]`][attr.pyfunction.html]. /// /// For more on creating class methods see the [class section of the guide][1]. @@ -212,11 +212,12 @@ pub fn pymethods_with_inventory(_: TokenStream, input: TokenStream) -> TokenStre /// A proc macro used to expose Rust functions to Python. /// -/// Functions annotated with `#[pyfunction]` can also be annotated with the following: +/// Functions annotated with `#[pyfunction]` can also be annotated with the following `#[pyo3]` options: /// /// | Annotation | Description | /// | :- | :- | /// | `#[pyo3(name = "...")]` | Defines the name of the function in Python. | +/// | `#[pyo3(text_signature = "...")]` | Defines the `__text_signature__` attribute of the function in Python. | /// /// For more on exposing functions see the [function section of the guide][1]. /// diff --git a/src/derive_utils.rs b/src/derive_utils.rs index c328bce0c3f..a175fd3d304 100644 --- a/src/derive_utils.rs +++ b/src/derive_utils.rs @@ -294,7 +294,7 @@ impl ModuleDef { /// Make new module defenition with given module name. /// /// # Safety - /// `name` must be a null-terminated string. + /// `name` and `doc` must be null-terminated strings. pub const unsafe fn new(name: &'static str, doc: &'static str) -> Self { const INIT: ffi::PyModuleDef = ffi::PyModuleDef { m_base: ffi::PyModuleDef_HEAD_INIT, diff --git a/src/impl_/deprecations.rs b/src/impl_/deprecations.rs index 94707455f05..af1b73487d1 100644 --- a/src/impl_/deprecations.rs +++ b/src/impl_/deprecations.rs @@ -11,3 +11,9 @@ pub const NAME_ATTRIBUTE: () = (); note = "use `#[pyfn(m)] #[pyo3(name = \"...\")]` instead of `#[pyfn(m, \"...\")]`" )] pub const PYFN_NAME_ARGUMENT: () = (); + +#[deprecated( + since = "0.14.0", + note = "use `#[pyo3(text_signature = \"...\")]` instead of `#[text_signature = \"...\"]`" +)] +pub const TEXT_SIGNATURE_ATTRIBUTE: () = (); diff --git a/tests/test_text_signature.rs b/tests/test_text_signature.rs index 13fa656450c..1c55216fcc8 100644 --- a/tests/test_text_signature.rs +++ b/tests/test_text_signature.rs @@ -37,7 +37,7 @@ fn class_with_docs_and_signature() { /// docs line1 #[pyclass] /// docs line2 - #[text_signature = "(a, b=None, *, c=42)"] + #[pyo3(text_signature = "(a, b=None, *, c=42)")] /// docs line3 struct MyClass {} @@ -71,7 +71,7 @@ fn class_with_docs_and_signature() { #[cfg_attr(all(Py_LIMITED_API, not(Py_3_10)), ignore)] fn class_with_signature() { #[pyclass] - #[text_signature = "(a, b=None, *, c=42)"] + #[pyo3(text_signature = "(a, b=None, *, c=42)")] struct MyClass {} #[pymethods] @@ -103,7 +103,7 @@ fn class_with_signature() { #[test] fn test_function() { #[pyfunction(a, b = "None", "*", c = 42)] - #[text_signature = "(a, b=None, *, c=42)"] + #[pyo3(text_signature = "(a, b=None, *, c=42)")] fn my_function(a: i32, b: Option, c: i32) { let _ = (a, b, c); } @@ -120,7 +120,7 @@ fn test_pyfn() { #[pymodule] fn my_module(_py: Python, m: &PyModule) -> PyResult<()> { #[pyfn(m, a, b = "None", "*", c = 42)] - #[text_signature = "(a, b=None, *, c=42)"] + #[pyo3(text_signature = "(a, b=None, *, c=42)")] fn my_function(a: i32, b: Option, c: i32) { let _ = (a, b, c); } @@ -145,21 +145,21 @@ fn test_methods() { #[pymethods] impl MyClass { - #[text_signature = "($self, a)"] + #[pyo3(text_signature = "($self, a)")] fn method(&self, a: i32) { let _ = a; } - #[text_signature = "($self, b)"] + #[pyo3(text_signature = "($self, b)")] fn pyself_method(_this: &PyCell, b: i32) { let _ = b; } #[classmethod] - #[text_signature = "($cls, c)"] + #[pyo3(text_signature = "($cls, c)")] fn class_method(_cls: &PyType, c: i32) { let _ = c; } #[staticmethod] - #[text_signature = "(d)"] + #[pyo3(text_signature = "(d)")] fn static_method(d: i32) { let _ = d; } @@ -195,7 +195,7 @@ fn test_methods() { #[cfg_attr(all(Py_LIMITED_API, not(Py_3_10)), ignore)] fn test_raw_identifiers() { #[pyclass] - #[text_signature = "($self)"] + #[pyo3(text_signature = "($self)")] struct r#MyClass {} #[pymethods] @@ -204,7 +204,7 @@ fn test_raw_identifiers() { fn new() -> MyClass { MyClass {} } - #[text_signature = "($self)"] + #[pyo3(text_signature = "($self)")] fn r#method(&self) {} } diff --git a/tests/ui/deprecations.rs b/tests/ui/deprecations.rs index eda305d4ae9..8dc86dae821 100644 --- a/tests/ui/deprecations.rs +++ b/tests/ui/deprecations.rs @@ -3,6 +3,7 @@ use pyo3::prelude::*; #[pyclass] +#[text_signature = "()"] struct TestClass { num: u32, } @@ -14,20 +15,24 @@ impl TestClass { const DEPRECATED_NAME_CONSTANT: i32 = 0; #[name = "num"] + #[text_signature = "()"] fn deprecated_name_pymethod(&self) { } #[staticmethod] #[name = "custom_static"] + #[text_signature = "()"] fn deprecated_name_staticmethod() {} } #[pyfunction] #[name = "foo"] +#[text_signature = "()"] fn deprecated_name_pyfunction() { } #[pymodule] fn my_module(_py: Python, m: &PyModule) -> PyResult<()> { #[pyfn(m, "some_name")] + #[text_signature = "()"] fn deprecated_name_pyfn() { } Ok(()) diff --git a/tests/ui/deprecations.stderr b/tests/ui/deprecations.stderr index 5ace1089d84..e330c8df863 100644 --- a/tests/ui/deprecations.stderr +++ b/tests/ui/deprecations.stderr @@ -1,7 +1,7 @@ error: use of deprecated constant `pyo3::impl_::deprecations::NAME_ATTRIBUTE`: use `#[pyo3(name = "...")]` instead of `#[name = "..."]` - --> $DIR/deprecations.rs:13:5 + --> $DIR/deprecations.rs:14:5 | -13 | #[name = "num"] +14 | #[name = "num"] | ^ | note: the lint level is defined here @@ -11,25 +11,55 @@ note: the lint level is defined here | ^^^^^^^^^^ error: use of deprecated constant `pyo3::impl_::deprecations::NAME_ATTRIBUTE`: use `#[pyo3(name = "...")]` instead of `#[name = "..."]` - --> $DIR/deprecations.rs:16:5 + --> $DIR/deprecations.rs:17:5 | -16 | #[name = "num"] +17 | #[name = "num"] + | ^ + +error: use of deprecated constant `pyo3::impl_::deprecations::TEXT_SIGNATURE_ATTRIBUTE`: use `#[pyo3(text_signature = "...")]` instead of `#[text_signature = "..."]` + --> $DIR/deprecations.rs:18:5 + | +18 | #[text_signature = "()"] | ^ error: use of deprecated constant `pyo3::impl_::deprecations::NAME_ATTRIBUTE`: use `#[pyo3(name = "...")]` instead of `#[name = "..."]` - --> $DIR/deprecations.rs:20:5 + --> $DIR/deprecations.rs:22:5 | -20 | #[name = "custom_static"] +22 | #[name = "custom_static"] + | ^ + +error: use of deprecated constant `pyo3::impl_::deprecations::TEXT_SIGNATURE_ATTRIBUTE`: use `#[pyo3(text_signature = "...")]` instead of `#[text_signature = "..."]` + --> $DIR/deprecations.rs:23:5 + | +23 | #[text_signature = "()"] | ^ error: use of deprecated constant `pyo3::impl_::deprecations::NAME_ATTRIBUTE`: use `#[pyo3(name = "...")]` instead of `#[name = "..."]` - --> $DIR/deprecations.rs:25:1 + --> $DIR/deprecations.rs:28:1 | -25 | #[name = "foo"] +28 | #[name = "foo"] + | ^ + +error: use of deprecated constant `pyo3::impl_::deprecations::TEXT_SIGNATURE_ATTRIBUTE`: use `#[pyo3(text_signature = "...")]` instead of `#[text_signature = "..."]` + --> $DIR/deprecations.rs:29:1 + | +29 | #[text_signature = "()"] | ^ error: use of deprecated constant `pyo3::impl_::deprecations::PYFN_NAME_ARGUMENT`: use `#[pyfn(m)] #[pyo3(name = "...")]` instead of `#[pyfn(m, "...")]` - --> $DIR/deprecations.rs:30:15 + --> $DIR/deprecations.rs:34:15 | -30 | #[pyfn(m, "some_name")] +34 | #[pyfn(m, "some_name")] | ^^^^^^^^^^^ + +error: use of deprecated constant `pyo3::impl_::deprecations::TEXT_SIGNATURE_ATTRIBUTE`: use `#[pyo3(text_signature = "...")]` instead of `#[text_signature = "..."]` + --> $DIR/deprecations.rs:35:5 + | +35 | #[text_signature = "()"] + | ^ + +error: use of deprecated constant `pyo3::impl_::deprecations::TEXT_SIGNATURE_ATTRIBUTE`: use `#[pyo3(text_signature = "...")]` instead of `#[text_signature = "..."]` + --> $DIR/deprecations.rs:6:1 + | +6 | #[text_signature = "()"] + | ^ diff --git a/tests/ui/invalid_pymethods.rs b/tests/ui/invalid_pymethods.rs index 43e7c838d7e..ebb2fb87463 100644 --- a/tests/ui/invalid_pymethods.rs +++ b/tests/ui/invalid_pymethods.rs @@ -48,35 +48,35 @@ impl MyClass { #[pymethods] impl MyClass { #[new] - #[text_signature = "()"] + #[pyo3(text_signature = "()")] fn text_signature_on_new() {} } #[pymethods] impl MyClass { #[call] - #[text_signature = "()"] + #[pyo3(text_signature = "()")] fn text_signature_on_call(&self) {} } #[pymethods] impl MyClass { #[getter(x)] - #[text_signature = "()"] + #[pyo3(text_signature = "()")] fn text_signature_on_getter(&self) {} } #[pymethods] impl MyClass { #[setter(x)] - #[text_signature = "()"] + #[pyo3(text_signature = "()")] fn text_signature_on_setter(&self) {} } #[pymethods] impl MyClass { #[classattr] - #[text_signature = "()"] + #[pyo3(text_signature = "()")] fn text_signature_on_classattr() {} } diff --git a/tests/ui/invalid_pymethods.stderr b/tests/ui/invalid_pymethods.stderr index c1ec13c618d..8091348e017 100644 --- a/tests/ui/invalid_pymethods.stderr +++ b/tests/ui/invalid_pymethods.stderr @@ -35,34 +35,34 @@ error: expected receiver for #[setter] | ^^ error: text_signature not allowed on __new__; if you want to add a signature on __new__, put it on the struct definition instead - --> $DIR/invalid_pymethods.rs:51:24 + --> $DIR/invalid_pymethods.rs:51:12 | -51 | #[text_signature = "()"] - | ^^^^ +51 | #[pyo3(text_signature = "()")] + | ^^^^^^^^^^^^^^ error: text_signature not allowed with this method type - --> $DIR/invalid_pymethods.rs:58:24 + --> $DIR/invalid_pymethods.rs:58:12 | -58 | #[text_signature = "()"] - | ^^^^ +58 | #[pyo3(text_signature = "()")] + | ^^^^^^^^^^^^^^ error: text_signature not allowed with this method type - --> $DIR/invalid_pymethods.rs:65:24 + --> $DIR/invalid_pymethods.rs:65:12 | -65 | #[text_signature = "()"] - | ^^^^ +65 | #[pyo3(text_signature = "()")] + | ^^^^^^^^^^^^^^ error: text_signature not allowed with this method type - --> $DIR/invalid_pymethods.rs:72:24 + --> $DIR/invalid_pymethods.rs:72:12 | -72 | #[text_signature = "()"] - | ^^^^ +72 | #[pyo3(text_signature = "()")] + | ^^^^^^^^^^^^^^ error: text_signature not allowed with this method type - --> $DIR/invalid_pymethods.rs:79:24 + --> $DIR/invalid_pymethods.rs:79:12 | -79 | #[text_signature = "()"] - | ^^^^ +79 | #[pyo3(text_signature = "()")] + | ^^^^^^^^^^^^^^ error: cannot specify a second method type --> $DIR/invalid_pymethods.rs:86:7