text_signature: move to #[pyo3(text_signature = "...")]

This commit is contained in:
David Hewitt 2021-06-05 16:28:31 +01:00
parent a5810eaffa
commit cec4c2d2e9
19 changed files with 317 additions and 171 deletions

View file

@ -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 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 `#[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 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 ### 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) - 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) - 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) - 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) - 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 ### Removed
- Remove deprecated exception names `BaseException` etc. [#1426](https://github.com/PyO3/pyo3/pull/1426) - Remove deprecated exception names `BaseException` etc. [#1426](https://github.com/PyO3/pyo3/pull/1426)

View file

@ -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 Due to limitations in the Python API, there are a few `pyo3` features that do
not work when compiling for `abi3`. These are: 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 `dict` and `weakref` options on classes are not supported until Python 3.9 or greater.
- The buffer API is not supported. - The buffer API is not supported.
- Optimizations which rely on knowledge of the exact Python version compiled against. - Optimizations which rely on knowledge of the exact Python version compiled against.

View file

@ -117,7 +117,7 @@ fn module_with_functions(py: Python, m: &PyModule) -> PyResult<()> {
## Making the function signature available to Python ## Making the function signature available to Python
In order to make the function signature available to Python to be retrieved via 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 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 is not a feature of this library in particular, but the general format used by
CPython for annotating signatures of built-in functions.) CPython for annotating signatures of built-in functions.)
@ -127,7 +127,7 @@ use pyo3::prelude::*;
/// This function adds two unsigned 64-bit integers. /// This function adds two unsigned 64-bit integers.
#[pyfunction] #[pyfunction]
#[text_signature = "(a, b, /)"] #[pyo3(text_signature = "(a, b, /)")]
fn add(a: u64, b: u64) -> u64 { fn add(a: u64, b: u64) -> u64 {
a + b a + b
} }
@ -142,7 +142,7 @@ use pyo3::types::PyType;
// it works even if the item is not documented: // it works even if the item is not documented:
#[pyclass] #[pyclass]
#[text_signature = "(c, d, /)"] #[pyo3(text_signature = "(c, d, /)")]
struct MyClass {} struct MyClass {}
#[pymethods] #[pymethods]
@ -154,17 +154,17 @@ impl MyClass {
Self {} Self {}
} }
// the self argument should be written $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 { fn my_method(&self, e: i32, f: i32) -> i32 {
e + f e + f
} }
#[classmethod] #[classmethod]
#[text_signature = "(cls, e, f)"] #[pyo3(text_signature = "(cls, e, f)")]
fn my_class_method(cls: &PyType, e: i32, f: i32) -> i32 { fn my_class_method(cls: &PyType, e: i32, f: i32) -> i32 {
e + f e + f
} }
#[staticmethod] #[staticmethod]
#[text_signature = "(e, f)"] #[pyo3(text_signature = "(e, f)")]
fn my_static_method(e: i32, f: i32) -> i32 { fn my_static_method(e: i32, f: i32) -> i32 {
e + f 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 formatted like in the following example. Please note that the newline after the
`--` is mandatory. The `/` signifies the end of positional-only arguments. `--` 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. generated signatures when those are added in a future version of PyO3.
```rust ```rust

View file

@ -18,6 +18,7 @@ pub mod kw {
syn::custom_keyword!(name); syn::custom_keyword!(name);
syn::custom_keyword!(set); syn::custom_keyword!(set);
syn::custom_keyword!(signature); syn::custom_keyword!(signature);
syn::custom_keyword!(text_signature);
syn::custom_keyword!(transparent); 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<Self> {
Ok(TextSignatureAttribute {
kw: input.parse()?,
eq_token: input.parse()?,
lit: input.parse()?,
})
}
}
pub fn get_pyo3_options<T: Parse>(attr: &syn::Attribute) -> Result<Option<Punctuated<T, Comma>>> { pub fn get_pyo3_options<T: Parse>(attr: &syn::Attribute) -> Result<Option<Punctuated<T, Comma>>> {
if is_attribute_ident(attr, "pyo3") { if is_attribute_ident(attr, "pyo3") {
attr.parse_args_with(Punctuated::parse_terminated).map(Some) attr.parse_args_with(Punctuated::parse_terminated).map(Some)
@ -112,3 +130,49 @@ pub fn get_deprecated_name_attribute(
_ => Ok(None), _ => Ok(None),
} }
} }
pub fn get_deprecated_text_signature_attribute(
attr: &syn::Attribute,
deprecations: &mut Deprecations,
) -> syn::Result<Option<TextSignatureAttribute>> {
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<syn::Attribute>,
deprecations: &mut Deprecations,
) -> syn::Result<Option<TextSignatureAttribute>> {
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)
}

View file

@ -4,6 +4,7 @@ use quote::{quote_spanned, ToTokens};
pub enum Deprecation { pub enum Deprecation {
NameAttribute, NameAttribute,
PyfnNameArgument, PyfnNameArgument,
TextSignatureAttribute,
} }
impl Deprecation { impl Deprecation {
@ -11,6 +12,7 @@ impl Deprecation {
let string = match self { let string = match self {
Deprecation::NameAttribute => "NAME_ATTRIBUTE", Deprecation::NameAttribute => "NAME_ATTRIBUTE",
Deprecation::PyfnNameArgument => "PYFN_NAME_ARGUMENT", Deprecation::PyfnNameArgument => "PYFN_NAME_ARGUMENT",
Deprecation::TextSignatureAttribute => "TEXT_SIGNATURE_ATTRIBUTE",
}; };
syn::Ident::new(string, span) syn::Ident::new(string, span)
} }

View file

@ -1,5 +1,6 @@
// Copyright (c) 2017-present PyO3 Project and Contributors // Copyright (c) 2017-present PyO3 Project and Contributors
use crate::attributes::TextSignatureAttribute;
use crate::pyfunction::PyFunctionOptions; use crate::pyfunction::PyFunctionOptions;
use crate::pyfunction::{PyFunctionArgPyO3Attributes, PyFunctionSignature}; use crate::pyfunction::{PyFunctionArgPyO3Attributes, PyFunctionSignature};
use crate::utils; 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)?; 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 name = &sig.ident;
let ty = get_return_info(&sig.output); let ty = get_return_info(&sig.output);
let python_name = python_name.as_ref().unwrap_or(name).unraw(); 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(
let doc = utils::get_doc(&meth_attrs, text_signature, true)?; &meth_attrs,
options
.text_signature
.as_ref()
.map(|attr| (&python_name, attr)),
)?;
let arguments = if skip_first_arg { let arguments = if skip_first_arg {
sig.inputs sig.inputs
@ -246,36 +253,27 @@ impl<'a> FnSpec<'a> {
syn::LitStr::new(&format!("{}\0", self.python_name), self.python_name.span()) syn::LitStr::new(&format!("{}\0", self.python_name), self.python_name.span())
} }
fn parse_text_signature( fn ensure_text_signature_on_valid_method(
meth_attrs: &mut Vec<syn::Attribute>,
fn_type: &FnType, fn_type: &FnType,
python_name: &syn::Ident, text_signature: Option<&TextSignatureAttribute>,
) -> syn::Result<Option<syn::LitStr>> { ) -> syn::Result<()> {
let mut parse_erroneous_text_signature = |error_msg: &str| { if let Some(text_signature) = text_signature {
// try to parse anyway to give better error messages match &fn_type {
if let Some(text_signature) = FnType::FnNew => bail_spanned!(
utils::parse_text_signature_attrs(meth_attrs, &python_name)? text_signature.kw.span() =>
{ "text_signature not allowed on __new__; if you want to add a signature on \
bail_spanned!(text_signature.span() => error_msg) __new__, put it on the struct definition instead"
} else { ),
Ok(None) FnType::FnCall(_)
| FnType::Getter(_)
| FnType::Setter(_)
| FnType::ClassAttribute => bail_spanned!(
text_signature.kw.span() => "text_signature not allowed with this method type"
),
_ => {}
} }
}; }
Ok(())
let text_signature = match &fn_type {
FnType::Fn(_) | FnType::FnClass | FnType::FnStatic => {
utils::parse_text_signature_attrs(&mut *meth_attrs, &python_name)?
}
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)
} }
fn parse_fn_type( fn parse_fn_type(

View file

@ -14,6 +14,7 @@ use syn::{parse::Parse, spanned::Spanned, token::Comma, Ident, Path};
/// module /// module
pub fn py_init(fnname: &Ident, name: &Ident, doc: syn::LitStr) -> TokenStream { pub fn py_init(fnname: &Ident, name: &Ident, doc: syn::LitStr) -> TokenStream {
let cb_name = Ident::new(&format!("PyInit_{}", name), Span::call_site()); let cb_name = Ident::new(&format!("PyInit_{}", name), Span::call_site());
assert!(doc.value().ends_with('\0'));
quote! { quote! {
#[no_mangle] #[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 { pub unsafe extern "C" fn #cb_name() -> *mut pyo3::ffi::PyObject {
use pyo3::derive_utils::ModuleDef; use pyo3::derive_utils::ModuleDef;
static NAME: &str = concat!(stringify!(#name), "\0"); 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) }; static MODULE_DEF: ModuleDef = unsafe { ModuleDef::new(NAME, DOC) };
pyo3::callback::handle_panic(|_py| { MODULE_DEF.make_module(_py, #fnname) }) pyo3::callback::handle_panic(|_py| { MODULE_DEF.make_module(_py, #fnname) })

View file

@ -1,6 +1,10 @@
// Copyright (c) 2017-present PyO3 Project and Contributors // 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::pyimpl::PyClassMethodsType;
use crate::pymethod::{impl_py_getter_def, impl_py_setter_def, PropertyType}; use crate::pymethod::{impl_py_getter_def, impl_py_setter_def, PropertyType};
use crate::utils; use crate::utils;
@ -162,16 +166,71 @@ impl PyClassArgs {
} }
} }
#[derive(Default)]
pub struct PyClassPyO3Options {
pub text_signature: Option<TextSignatureAttribute>,
pub deprecations: Deprecations,
}
enum PyClassPyO3Option {
TextSignature(TextSignatureAttribute),
}
impl Parse for PyClassPyO3Option {
fn parse(input: ParseStream) -> Result<Self> {
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::Attribute>) -> syn::Result<Self> {
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( pub fn build_py_class(
class: &mut syn::ItemStruct, class: &mut syn::ItemStruct,
attr: &PyClassArgs, args: &PyClassArgs,
methods_type: PyClassMethodsType, methods_type: PyClassMethodsType,
) -> syn::Result<TokenStream> { ) -> syn::Result<TokenStream> {
let text_signature = utils::parse_text_signature_attrs( let mut options = PyClassPyO3Options::take_pyo3_options(&mut class.attrs)?;
&mut class.attrs, if let Some(text_signature) =
&get_class_python_name(&class.ident, attr), 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!( ensure_spanned!(
class.generics.params.is_empty(), 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 /// `#[pyo3()]` options for pyclass fields
@ -308,6 +374,7 @@ fn impl_class(
doc: syn::LitStr, doc: syn::LitStr,
field_options: Vec<(&syn::Field, FieldPyO3Options)>, field_options: Vec<(&syn::Field, FieldPyO3Options)>,
methods_type: PyClassMethodsType, methods_type: PyClassMethodsType,
deprecations: Deprecations,
) -> syn::Result<TokenStream> { ) -> syn::Result<TokenStream> {
let cls_name = get_class_python_name(cls, attr).to_string(); let cls_name = get_class_python_name(cls, attr).to_string();
@ -429,6 +496,8 @@ fn impl_class(
#[inline] #[inline]
fn type_object_raw(py: pyo3::Python) -> *mut pyo3::ffi::PyTypeObject { fn type_object_raw(py: pyo3::Python) -> *mut pyo3::ffi::PyTypeObject {
#deprecations
use pyo3::type_object::LazyStaticType; use pyo3::type_object::LazyStaticType;
static TYPE_OBJECT: LazyStaticType = LazyStaticType::new(); static TYPE_OBJECT: LazyStaticType = LazyStaticType::new();
TYPE_OBJECT.get_or_init::<Self>(py) TYPE_OBJECT.get_or_init::<Self>(py)

View file

@ -2,8 +2,9 @@
use crate::{ use crate::{
attributes::{ attributes::{
self, get_deprecated_name_attribute, get_pyo3_options, take_attributes, self, get_deprecated_name_attribute, get_deprecated_text_signature_attribute,
FromPyWithAttribute, NameAttribute, get_pyo3_options, take_attributes, FromPyWithAttribute, NameAttribute,
TextSignatureAttribute,
}, },
deprecations::Deprecations, deprecations::Deprecations,
method::{self, FnArg, FnSpec}, method::{self, FnArg, FnSpec},
@ -211,6 +212,7 @@ pub struct PyFunctionOptions {
pub pass_module: bool, pub pass_module: bool,
pub name: Option<NameAttribute>, pub name: Option<NameAttribute>,
pub signature: Option<PyFunctionSignature>, pub signature: Option<PyFunctionSignature>,
pub text_signature: Option<TextSignatureAttribute>,
pub deprecations: Deprecations, pub deprecations: Deprecations,
} }
@ -220,6 +222,7 @@ impl Parse for PyFunctionOptions {
pass_module: false, pass_module: false,
name: None, name: None,
signature: None, signature: None,
text_signature: None,
deprecations: Deprecations::new(), deprecations: Deprecations::new(),
}; };
@ -228,6 +231,7 @@ impl Parse for PyFunctionOptions {
if lookahead.peek(attributes::kw::name) if lookahead.peek(attributes::kw::name)
|| lookahead.peek(attributes::kw::pass_module) || lookahead.peek(attributes::kw::pass_module)
|| lookahead.peek(attributes::kw::signature) || lookahead.peek(attributes::kw::signature)
|| lookahead.peek(attributes::kw::text_signature)
{ {
options.add_attributes(std::iter::once(input.parse()?))?; options.add_attributes(std::iter::once(input.parse()?))?;
if !input.is_empty() { if !input.is_empty() {
@ -250,6 +254,7 @@ pub enum PyFunctionOption {
Name(NameAttribute), Name(NameAttribute),
PassModule(attributes::kw::pass_module), PassModule(attributes::kw::pass_module),
Signature(PyFunctionSignature), Signature(PyFunctionSignature),
TextSignature(TextSignatureAttribute),
} }
impl Parse for PyFunctionOption { impl Parse for PyFunctionOption {
@ -261,6 +266,8 @@ impl Parse for PyFunctionOption {
input.parse().map(PyFunctionOption::PassModule) input.parse().map(PyFunctionOption::PassModule)
} else if lookahead.peek(attributes::kw::signature) { } else if lookahead.peek(attributes::kw::signature) {
input.parse().map(PyFunctionOption::Signature) input.parse().map(PyFunctionOption::Signature)
} else if lookahead.peek(attributes::kw::text_signature) {
input.parse().map(PyFunctionOption::TextSignature)
} else { } else {
Err(lookahead.error()) Err(lookahead.error())
} }
@ -283,6 +290,13 @@ impl PyFunctionOptions {
{ {
self.set_name(name)?; self.set_name(name)?;
Ok(true) 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 { } else {
Ok(false) Ok(false)
} }
@ -313,6 +327,13 @@ impl PyFunctionOptions {
); );
self.signature = Some(signature); 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(()) Ok(())
@ -379,8 +400,13 @@ pub fn impl_wrap_pyfunction(
let ty = method::get_return_info(&func.sig.output); 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(
let doc = utils::get_doc(&func.attrs, text_signature, true)?; &func.attrs,
options
.text_signature
.as_ref()
.map(|attr| (&python_name, attr)),
)?;
let function_wrapper_ident = function_wrapper_ident(&func.sig.ident); let function_wrapper_ident = function_wrapper_ident(&func.sig.ident);

View file

@ -876,7 +876,7 @@ impl PropertyType<'_> {
fn doc(&self) -> Cow<syn::LitStr> { fn doc(&self) -> Cow<syn::LitStr> {
match self { match self {
PropertyType::Descriptor { field, .. } => { 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())); .unwrap_or_else(|_| syn::LitStr::new("", Span::call_site()));
Cow::Owned(doc) Cow::Owned(doc)
} }

View file

@ -2,6 +2,8 @@
use proc_macro2::Span; use proc_macro2::Span;
use syn::spanned::Spanned; use syn::spanned::Spanned;
use crate::attributes::TextSignatureAttribute;
/// Macro inspired by `anyhow::anyhow!` to create a compiler error with the given span. /// Macro inspired by `anyhow::anyhow!` to create a compiler error with the given span.
macro_rules! err_spanned { macro_rules! err_spanned {
($span:expr => $msg:expr) => { ($span:expr => $msg:expr) => {
@ -56,79 +58,19 @@ pub fn option_type_argument(ty: &syn::Type) -> Option<&syn::Type> {
None None
} }
pub fn is_text_signature_attr(attr: &syn::Attribute) -> bool { // Returns a null-terminated syn::LitStr for use as a Python docstring.
attr.path.is_ident("text_signature")
}
fn parse_text_signature_attr(
attr: &syn::Attribute,
python_name: &syn::Ident,
) -> syn::Result<Option<syn::LitStr>> {
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<syn::Attribute>,
python_name: &syn::Ident,
) -> syn::Result<Option<syn::LitStr>> {
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.
pub fn get_doc( pub fn get_doc(
attrs: &[syn::Attribute], attrs: &[syn::Attribute],
text_signature: Option<syn::LitStr>, text_signature: Option<(&syn::Ident, &TextSignatureAttribute)>,
null_terminated: bool,
) -> syn::Result<syn::LitStr> { ) -> syn::Result<syn::LitStr> {
let mut doc = String::new(); let mut doc = String::new();
let mut span = Span::call_site(); 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__` // create special doc string lines to set `__text_signature__`
span = text_signature.span(); doc.push_str(&python_name.to_string());
doc.push_str(&text_signature.value()); span = text_signature.lit.span();
doc.push_str(&text_signature.lit.value());
doc.push_str("\n--\n\n"); doc.push_str("\n--\n\n");
} }
@ -136,9 +78,12 @@ pub fn get_doc(
let mut first = true; let mut first = true;
for attr in attrs.iter() { for attr in attrs.iter() {
if let Ok(syn::Meta::NameValue(metanv)) = attr.parse_meta() { if attr.path.is_ident("doc") {
if metanv.path.is_ident("doc") { match attr.parse_meta()? {
if let syn::Lit::Str(litstr) = metanv.lit { syn::Meta::NameValue(syn::MetaNameValue {
lit: syn::Lit::Str(litstr),
..
}) => {
if first { if first {
first = false; first = false;
span = litstr.span(); span = litstr.span();
@ -151,16 +96,13 @@ pub fn get_doc(
doc.push_str(&d); doc.push_str(&d);
}; };
separator = "\n"; 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)) Ok(syn::LitStr::new(&doc, span))
} }

View file

@ -32,7 +32,7 @@ pub fn pymodule(attr: TokenStream, input: TokenStream) -> TokenStream {
return err.to_compile_error().into(); 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, Ok(doc) => doc,
Err(err) => return err.to_compile_error().into(), 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. | /// | [`#[classattr]`][9] | Defines a class variable. |
/// | [`#[args]`][10] | Define a method's default arguments and allows the function to receive `*args` and `**kwargs`. | /// | [`#[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]. /// be used with [`#[pyfunction]`][attr.pyfunction.html].
/// ///
/// For more on creating class methods see the [class section of the guide][1]. /// 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. /// 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 | /// | Annotation | Description |
/// | :- | :- | /// | :- | :- |
/// | `#[pyo3(name = "...")]` | Defines the name of the function in Python. | /// | `#[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]. /// For more on exposing functions see the [function section of the guide][1].
/// ///

View file

@ -294,7 +294,7 @@ impl ModuleDef {
/// Make new module defenition with given module name. /// Make new module defenition with given module name.
/// ///
/// # Safety /// # 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 { pub const unsafe fn new(name: &'static str, doc: &'static str) -> Self {
const INIT: ffi::PyModuleDef = ffi::PyModuleDef { const INIT: ffi::PyModuleDef = ffi::PyModuleDef {
m_base: ffi::PyModuleDef_HEAD_INIT, m_base: ffi::PyModuleDef_HEAD_INIT,

View file

@ -11,3 +11,9 @@ pub const NAME_ATTRIBUTE: () = ();
note = "use `#[pyfn(m)] #[pyo3(name = \"...\")]` instead of `#[pyfn(m, \"...\")]`" note = "use `#[pyfn(m)] #[pyo3(name = \"...\")]` instead of `#[pyfn(m, \"...\")]`"
)] )]
pub const PYFN_NAME_ARGUMENT: () = (); pub const PYFN_NAME_ARGUMENT: () = ();
#[deprecated(
since = "0.14.0",
note = "use `#[pyo3(text_signature = \"...\")]` instead of `#[text_signature = \"...\"]`"
)]
pub const TEXT_SIGNATURE_ATTRIBUTE: () = ();

View file

@ -37,7 +37,7 @@ fn class_with_docs_and_signature() {
/// docs line1 /// docs line1
#[pyclass] #[pyclass]
/// docs line2 /// docs line2
#[text_signature = "(a, b=None, *, c=42)"] #[pyo3(text_signature = "(a, b=None, *, c=42)")]
/// docs line3 /// docs line3
struct MyClass {} struct MyClass {}
@ -71,7 +71,7 @@ fn class_with_docs_and_signature() {
#[cfg_attr(all(Py_LIMITED_API, not(Py_3_10)), ignore)] #[cfg_attr(all(Py_LIMITED_API, not(Py_3_10)), ignore)]
fn class_with_signature() { fn class_with_signature() {
#[pyclass] #[pyclass]
#[text_signature = "(a, b=None, *, c=42)"] #[pyo3(text_signature = "(a, b=None, *, c=42)")]
struct MyClass {} struct MyClass {}
#[pymethods] #[pymethods]
@ -103,7 +103,7 @@ fn class_with_signature() {
#[test] #[test]
fn test_function() { fn test_function() {
#[pyfunction(a, b = "None", "*", c = 42)] #[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<i32>, c: i32) { fn my_function(a: i32, b: Option<i32>, c: i32) {
let _ = (a, b, c); let _ = (a, b, c);
} }
@ -120,7 +120,7 @@ fn test_pyfn() {
#[pymodule] #[pymodule]
fn my_module(_py: Python, m: &PyModule) -> PyResult<()> { fn my_module(_py: Python, m: &PyModule) -> PyResult<()> {
#[pyfn(m, a, b = "None", "*", c = 42)] #[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<i32>, c: i32) { fn my_function(a: i32, b: Option<i32>, c: i32) {
let _ = (a, b, c); let _ = (a, b, c);
} }
@ -145,21 +145,21 @@ fn test_methods() {
#[pymethods] #[pymethods]
impl MyClass { impl MyClass {
#[text_signature = "($self, a)"] #[pyo3(text_signature = "($self, a)")]
fn method(&self, a: i32) { fn method(&self, a: i32) {
let _ = a; let _ = a;
} }
#[text_signature = "($self, b)"] #[pyo3(text_signature = "($self, b)")]
fn pyself_method(_this: &PyCell<Self>, b: i32) { fn pyself_method(_this: &PyCell<Self>, b: i32) {
let _ = b; let _ = b;
} }
#[classmethod] #[classmethod]
#[text_signature = "($cls, c)"] #[pyo3(text_signature = "($cls, c)")]
fn class_method(_cls: &PyType, c: i32) { fn class_method(_cls: &PyType, c: i32) {
let _ = c; let _ = c;
} }
#[staticmethod] #[staticmethod]
#[text_signature = "(d)"] #[pyo3(text_signature = "(d)")]
fn static_method(d: i32) { fn static_method(d: i32) {
let _ = d; let _ = d;
} }
@ -195,7 +195,7 @@ fn test_methods() {
#[cfg_attr(all(Py_LIMITED_API, not(Py_3_10)), ignore)] #[cfg_attr(all(Py_LIMITED_API, not(Py_3_10)), ignore)]
fn test_raw_identifiers() { fn test_raw_identifiers() {
#[pyclass] #[pyclass]
#[text_signature = "($self)"] #[pyo3(text_signature = "($self)")]
struct r#MyClass {} struct r#MyClass {}
#[pymethods] #[pymethods]
@ -204,7 +204,7 @@ fn test_raw_identifiers() {
fn new() -> MyClass { fn new() -> MyClass {
MyClass {} MyClass {}
} }
#[text_signature = "($self)"] #[pyo3(text_signature = "($self)")]
fn r#method(&self) {} fn r#method(&self) {}
} }

View file

@ -3,6 +3,7 @@
use pyo3::prelude::*; use pyo3::prelude::*;
#[pyclass] #[pyclass]
#[text_signature = "()"]
struct TestClass { struct TestClass {
num: u32, num: u32,
} }
@ -14,20 +15,24 @@ impl TestClass {
const DEPRECATED_NAME_CONSTANT: i32 = 0; const DEPRECATED_NAME_CONSTANT: i32 = 0;
#[name = "num"] #[name = "num"]
#[text_signature = "()"]
fn deprecated_name_pymethod(&self) { } fn deprecated_name_pymethod(&self) { }
#[staticmethod] #[staticmethod]
#[name = "custom_static"] #[name = "custom_static"]
#[text_signature = "()"]
fn deprecated_name_staticmethod() {} fn deprecated_name_staticmethod() {}
} }
#[pyfunction] #[pyfunction]
#[name = "foo"] #[name = "foo"]
#[text_signature = "()"]
fn deprecated_name_pyfunction() { } fn deprecated_name_pyfunction() { }
#[pymodule] #[pymodule]
fn my_module(_py: Python, m: &PyModule) -> PyResult<()> { fn my_module(_py: Python, m: &PyModule) -> PyResult<()> {
#[pyfn(m, "some_name")] #[pyfn(m, "some_name")]
#[text_signature = "()"]
fn deprecated_name_pyfn() { } fn deprecated_name_pyfn() { }
Ok(()) Ok(())

View file

@ -1,7 +1,7 @@
error: use of deprecated constant `pyo3::impl_::deprecations::NAME_ATTRIBUTE`: use `#[pyo3(name = "...")]` instead of `#[name = "..."]` 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 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 = "..."]` 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 = "..."]` 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 = "..."]` 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, "...")]` 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 = "()"]
| ^

View file

@ -48,35 +48,35 @@ impl MyClass {
#[pymethods] #[pymethods]
impl MyClass { impl MyClass {
#[new] #[new]
#[text_signature = "()"] #[pyo3(text_signature = "()")]
fn text_signature_on_new() {} fn text_signature_on_new() {}
} }
#[pymethods] #[pymethods]
impl MyClass { impl MyClass {
#[call] #[call]
#[text_signature = "()"] #[pyo3(text_signature = "()")]
fn text_signature_on_call(&self) {} fn text_signature_on_call(&self) {}
} }
#[pymethods] #[pymethods]
impl MyClass { impl MyClass {
#[getter(x)] #[getter(x)]
#[text_signature = "()"] #[pyo3(text_signature = "()")]
fn text_signature_on_getter(&self) {} fn text_signature_on_getter(&self) {}
} }
#[pymethods] #[pymethods]
impl MyClass { impl MyClass {
#[setter(x)] #[setter(x)]
#[text_signature = "()"] #[pyo3(text_signature = "()")]
fn text_signature_on_setter(&self) {} fn text_signature_on_setter(&self) {}
} }
#[pymethods] #[pymethods]
impl MyClass { impl MyClass {
#[classattr] #[classattr]
#[text_signature = "()"] #[pyo3(text_signature = "()")]
fn text_signature_on_classattr() {} fn text_signature_on_classattr() {}
} }

View file

@ -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 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 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 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 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 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 error: cannot specify a second method type
--> $DIR/invalid_pymethods.rs:86:7 --> $DIR/invalid_pymethods.rs:86:7