2784: Automatically generate `__text_signature__` for all functions r=davidhewitt a=davidhewitt

This PR makes it so that PyO3 generates `__text_signature__` by default for all functions. It also introduces `#[pyo3(text_signature = false)]` to disable the built-in generation.

There are a few limitations which we can improve later:
 - All default values are currently set to `...`. I think this is ok because `.pyi` files often do the same. Maybe for numbers, strings, `None` and `True`/`False` we could render these in a future PR.
 - No support for `#[new]` yet.

Alternative design ideas:
- Only autogenerate for methods with `#[pyo3(signature = (...))]` annotation. I started with this, and then decided it made sense to do it for everything.
- Opt-out with `#[pyo3(text_signature = None)]`. This is slightly harder to parse in the macro, but matches the final result in Python better, so if this looks preferable to others, I can change from `text_signature = false` to `text_signature = None`.

There's some small tidying up / refactoring to do before this merges (happy to take suggestions on this), however the general logic, design and docs are ready for review.


2827: pypy: enable `PyList::get_item_unchecked` r=adamreichold a=davidhewitt

Split out from #2826. Approved previously as part of that review.

Co-authored-by: David Hewitt <1939362+davidhewitt@users.noreply.github.com>
This commit is contained in:
bors[bot] 2022-12-25 19:45:50 +00:00 committed by GitHub
commit e5ae4e266b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 582 additions and 175 deletions

View File

@ -73,39 +73,7 @@ The `#[pyo3]` attribute can be used to modify properties of the generated Python
- <a name="text_signature"></a> `#[pyo3(text_signature = "...")]`
Sets the function signature visible in Python tooling (such as via [`inspect.signature`]).
The example below creates a function `add` which has a signature describing two positional-only
arguments `a` and `b`.
```rust
use pyo3::prelude::*;
/// This function adds two unsigned 64-bit integers.
#[pyfunction]
#[pyo3(text_signature = "(a, b, /)")]
fn add(a: u64, b: u64) -> u64 {
a + b
}
#
# fn main() -> PyResult<()> {
# Python::with_gil(|py| {
# let fun = pyo3::wrap_pyfunction!(add, py)?;
#
# let doc: String = fun.getattr("__doc__")?.extract()?;
# assert_eq!(doc, "This function adds two unsigned 64-bit integers.");
#
# let inspect = PyModule::import(py, "inspect")?.getattr("signature")?;
# let sig: String = inspect
# .call1((fun,))?
# .call_method0("__str__")?
# .extract()?;
# assert_eq!(sig, "(a, b, /)");
#
# Ok(())
# })
# }
```
Overrides the PyO3-generated function signature visible in Python tooling (such as via [`inspect.signature`]). See the [corresponding topic in the Function Signatures subchapter](./function/signature.md#making-the-function-signature-available-to-python).
- <a name="pass_module" ></a> `#[pyo3(pass_module)]`
@ -161,47 +129,6 @@ The `#[pyo3]` attribute can be used on individual arguments to modify properties
## Advanced function patterns
### Making the function signature available to Python (old method)
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.
`#[pyo3(text_signature)]` should be preferred, since it will override automatically
generated signatures when those are added in a future version of PyO3.
```rust
# #![allow(dead_code)]
use pyo3::prelude::*;
/// add(a, b, /)
/// --
///
/// This function adds two unsigned 64-bit integers.
#[pyfunction]
fn add(a: u64, b: u64) -> u64 {
a + b
}
// a function with a signature but without docs. Both blank lines after the `--` are mandatory.
/// sub(a, b, /)
/// --
#[pyfunction]
fn sub(a: u64, b: u64) -> u64 {
a - b
}
```
When annotated like this, signatures are also correctly displayed in IPython.
```text
>>> pyo3_test.add?
Signature: pyo3_test.add(a, b, /)
Docstring: This function adds two unsigned 64-bit integers.
Type: builtin_function_or_method
```
### Calling Python functions in Rust
You can pass Python `def`'d functions and built-in functions to Rust functions [`PyFunction`]

View File

@ -188,3 +188,138 @@ impl MyClass {
}
}
```
## Making the function signature available to Python
The function signature is exposed to Python via the `__text_signature__` attribute. PyO3 automatically generates this for every `#[pyfunction]` and all `#[pymethods]` directly from the Rust function, taking into account any override done with the `#[pyo3(signature = (...))]` option.
This automatic generation has some limitations, which may be improved in the future:
- It will not include the value of default arguments, replacing them all with `...`. (`.pyi` type stub files commonly also use `...` for all default arguments in the same way.)
- Nothing is generated for the `#[new]` method of a `#[pyclass]`.
In cases where the automatically-generated signature needs adjusting, it can [be overridden](#overriding-the-generated-signature) using the `#[pyo3(text_signature)]` option.)
The example below creates a function `add` which accepts two positional-only arguments `a` and `b`, where `b` has a default value of zero.
```rust
use pyo3::prelude::*;
/// This function adds two unsigned 64-bit integers.
#[pyfunction]
#[pyo3(signature = (a, b=0, /))]
fn add(a: u64, b: u64) -> u64 {
a + b
}
#
# fn main() -> PyResult<()> {
# Python::with_gil(|py| {
# let fun = pyo3::wrap_pyfunction!(add, py)?;
#
# let doc: String = fun.getattr("__doc__")?.extract()?;
# assert_eq!(doc, "This function adds two unsigned 64-bit integers.");
#
# let inspect = PyModule::import(py, "inspect")?.getattr("signature")?;
# let sig: String = inspect
# .call1((fun,))?
# .call_method0("__str__")?
# .extract()?;
#
# #[cfg(Py_3_8)] // on 3.7 the signature doesn't render b, upstream bug?
# assert_eq!(sig, "(a, b=Ellipsis, /)");
#
# Ok(())
# })
# }
```
The following IPython output demonstrates how this generated signature will be seen from Python tooling:
```text
>>> pyo3_test.add.__text_signature__
'(a, b=..., /)'
>>> pyo3_test.add?
Signature: pyo3_test.add(a, b=Ellipsis, /)
Docstring: This function adds two unsigned 64-bit integers.
Type: builtin_function_or_method
```
### Overriding the generated signature
The `#[pyo3(text_signature = "(<some signature>)")]` attribute can be used to override the default generated signature.
In the snippet below, the text signature attribute is used to include the default value of `0` for the argument `b`, instead of the automatically-generated default value of `...`:
```rust
use pyo3::prelude::*;
/// This function adds two unsigned 64-bit integers.
#[pyfunction]
#[pyo3(signature = (a, b=0, /), text_signature = "(a, b=0, /)")]
fn add(a: u64, b: u64) -> u64 {
a + b
}
#
# fn main() -> PyResult<()> {
# Python::with_gil(|py| {
# let fun = pyo3::wrap_pyfunction!(add, py)?;
#
# let doc: String = fun.getattr("__doc__")?.extract()?;
# assert_eq!(doc, "This function adds two unsigned 64-bit integers.");
#
# let inspect = PyModule::import(py, "inspect")?.getattr("signature")?;
# let sig: String = inspect
# .call1((fun,))?
# .call_method0("__str__")?
# .extract()?;
# assert_eq!(sig, "(a, b=0, /)");
#
# Ok(())
# })
# }
```
PyO3 will include the contents of the annotation unmodified as the `__text_signature`. Below shows how IPython will now present this (see the default value of 0 for b):
```text
>>> pyo3_test.add.__text_signature__
'(a, b=0, /)'
>>> pyo3_test.add?
Signature: pyo3_test.add(a, b=0, /)
Docstring: This function adds two unsigned 64-bit integers.
Type: builtin_function_or_method
```
If no signature is wanted at all, `#[pyo3(text_signature = None)]` will disable the built-in signature. The snippet below demonstrates use of this:
```rust
use pyo3::prelude::*;
/// This function adds two unsigned 64-bit integers.
#[pyfunction]
#[pyo3(signature = (a, b=0, /), text_signature = None)]
fn add(a: u64, b: u64) -> u64 {
a + b
}
#
# fn main() -> PyResult<()> {
# Python::with_gil(|py| {
# let fun = pyo3::wrap_pyfunction!(add, py)?;
#
# let doc: String = fun.getattr("__doc__")?.extract()?;
# assert_eq!(doc, "This function adds two unsigned 64-bit integers.");
# assert!(fun.getattr("__text_signature__")?.is_none());
#
# Ok(())
# })
# }
```
Now the function's `__text_signature__` will be set to `None`, and IPython will not display any signature in the help:
```text
>>> pyo3_test.add.__text_signature__ == None
True
>>> pyo3_test.add?
Docstring: This function adds two unsigned 64-bit integers.
Type: builtin_function_or_method
```

View File

@ -0,0 +1 @@
Automatically generate `__text_signature__` for all Python functions created using `#[pyfunction]` and `#[pymethods]`.

View File

@ -0,0 +1 @@
Add `PyList::get_item_unchecked` for PyPy.

View File

@ -81,11 +81,46 @@ impl ToTokens for NameLitStr {
}
}
/// Text signatue can be either a literal string or opt-in/out
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum TextSignatureAttributeValue {
Str(LitStr),
// `None` ident to disable automatic text signature generation
Disabled(Ident),
}
impl Parse for TextSignatureAttributeValue {
fn parse(input: ParseStream<'_>) -> Result<Self> {
if let Ok(lit_str) = input.parse::<LitStr>() {
return Ok(TextSignatureAttributeValue::Str(lit_str));
}
let err_span = match input.parse::<Ident>() {
Ok(ident) if ident == "None" => {
return Ok(TextSignatureAttributeValue::Disabled(ident));
}
Ok(other_ident) => other_ident.span(),
Err(e) => e.span(),
};
Err(err_spanned!(err_span => "expected a string literal or `None`"))
}
}
impl ToTokens for TextSignatureAttributeValue {
fn to_tokens(&self, tokens: &mut TokenStream) {
match self {
TextSignatureAttributeValue::Str(s) => s.to_tokens(tokens),
TextSignatureAttributeValue::Disabled(b) => b.to_tokens(tokens),
}
}
}
pub type ExtendsAttribute = KeywordAttribute<kw::extends, Path>;
pub type FreelistAttribute = KeywordAttribute<kw::freelist, Box<Expr>>;
pub type ModuleAttribute = KeywordAttribute<kw::module, LitStr>;
pub type NameAttribute = KeywordAttribute<kw::name, NameLitStr>;
pub type TextSignatureAttribute = KeywordAttribute<kw::text_signature, LitStr>;
pub type TextSignatureAttribute = KeywordAttribute<kw::text_signature, TextSignatureAttributeValue>;
impl<K: Parse + std::fmt::Debug, V: Parse> Parse for KeywordAttribute<K, V> {
fn parse(input: ParseStream<'_>) -> Result<Self> {

View File

@ -5,7 +5,9 @@ use std::borrow::Cow;
use crate::attributes::TextSignatureAttribute;
use crate::deprecations::{Deprecation, Deprecations};
use crate::params::impl_arg_params;
use crate::pyfunction::{DeprecatedArgs, FunctionSignature, PyFunctionArgPyO3Attributes};
use crate::pyfunction::{
text_signature_or_auto, DeprecatedArgs, FunctionSignature, PyFunctionArgPyO3Attributes,
};
use crate::pyfunction::{PyFunctionOptions, SignatureAttribute};
use crate::utils::{self, PythonDoc};
use proc_macro2::{Span, TokenStream};
@ -202,7 +204,7 @@ impl CallingConvention {
pub fn from_signature(signature: &FunctionSignature<'_>) -> Self {
if signature.python_signature.has_no_args() {
Self::Noargs
} else if signature.python_signature.accepts_kwargs {
} else if signature.python_signature.kwargs.is_some() {
// for functions that accept **kwargs, always prefer varargs
Self::Varargs
} else if cfg!(not(feature = "abi3")) {
@ -288,13 +290,6 @@ impl<'a> FnSpec<'a> {
let ty = get_return_info(&sig.output);
let python_name = python_name.as_ref().unwrap_or(name).unraw();
let doc = utils::get_doc(
meth_attrs,
text_signature
.as_ref()
.map(|attr| (Cow::Borrowed(&python_name), attr)),
);
let arguments: Vec<_> = if skip_first_arg {
sig.inputs
.iter_mut()
@ -320,6 +315,16 @@ impl<'a> FnSpec<'a> {
FunctionSignature::from_arguments(arguments)
};
let text_signature_string = match &fn_type {
FnType::FnNew | FnType::Getter(_) | FnType::Setter(_) | FnType::ClassAttribute => None,
_ => text_signature_or_auto(text_signature.as_ref(), &signature, &fn_type),
};
let doc = utils::get_doc(
meth_attrs,
text_signature_string.map(|sig| (Cow::Borrowed(&python_name), sig)),
);
let convention =
fixed_convention.unwrap_or_else(|| CallingConvention::from_signature(&signature));

View File

@ -82,12 +82,12 @@ pub fn impl_arg_params(
.map(|arg| impl_arg_param(arg, &mut option_pos, py, &args_array))
.collect::<Result<_>>()?;
let args_handler = if spec.signature.python_signature.accepts_varargs {
let args_handler = if spec.signature.python_signature.varargs.is_some() {
quote! { _pyo3::impl_::extract_argument::TupleVarargs }
} else {
quote! { _pyo3::impl_::extract_argument::NoVarargs }
};
let kwargs_handler = if spec.signature.python_signature.accepts_kwargs {
let kwargs_handler = if spec.signature.python_signature.kwargs.is_some() {
quote! { _pyo3::impl_::extract_argument::DictVarkeywords }
} else {
quote! { _pyo3::impl_::extract_argument::NoVarkeywords }

View File

@ -9,6 +9,7 @@ use crate::attributes::{
use crate::deprecations::{Deprecation, Deprecations};
use crate::konst::{ConstAttributes, ConstSpec};
use crate::method::FnSpec;
use crate::pyfunction::text_signature_or_none;
use crate::pyimpl::{gen_py_const, PyClassMethodsType};
use crate::pymethod::{
impl_py_getter_def, impl_py_setter_def, MethodAndMethodDef, MethodAndSlotDef, PropertyType,
@ -198,12 +199,10 @@ pub fn build_py_class(
methods_type: PyClassMethodsType,
) -> syn::Result<TokenStream> {
args.options.take_pyo3_options(&mut class.attrs)?;
let text_signature_string = text_signature_or_none(args.options.text_signature.as_ref());
let doc = utils::get_doc(
&class.attrs,
args.options
.text_signature
.as_ref()
.map(|attr| (get_class_python_name(&class.ident, &args), attr)),
text_signature_string.map(|s| (get_class_python_name(&class.ident, &args), s)),
);
let krate = get_pyo3_crate(&args.options.krate);
@ -461,12 +460,11 @@ pub fn build_py_enum(
bail_spanned!(enum_.brace_token.span => "#[pyclass] can't be used on enums without any variants");
}
let text_signature_string = text_signature_or_none(args.options.text_signature.as_ref());
let doc = utils::get_doc(
&enum_.attrs,
args.options
.text_signature
.as_ref()
.map(|attr| (get_class_python_name(&enum_.ident, &args), attr)),
text_signature_string.map(|s| (get_class_python_name(&enum_.ident, &args), s)),
);
let enum_ = PyClassEnum::new(enum_)?;
impl_enum(enum_, &args, doc, method_type)

View File

@ -5,10 +5,10 @@ use std::borrow::Cow;
use crate::{
attributes::{
self, get_pyo3_options, take_attributes, take_pyo3_options, CrateAttribute,
FromPyWithAttribute, NameAttribute, TextSignatureAttribute,
FromPyWithAttribute, NameAttribute, TextSignatureAttribute, TextSignatureAttributeValue,
},
deprecations::{Deprecation, Deprecations},
method::{self, CallingConvention, FnArg},
method::{self, CallingConvention, FnArg, FnType},
pymethod::check_generic,
utils::{self, ensure_not_async_fn, get_pyo3_crate},
};
@ -409,11 +409,11 @@ pub fn impl_wrap_pyfunction(
let ty = method::get_return_info(&func.sig.output);
let text_signature_string = text_signature_or_auto(text_signature.as_ref(), &signature, &tp);
let doc = utils::get_doc(
&func.attrs,
text_signature
.as_ref()
.map(|attr| (Cow::Borrowed(&python_name), attr)),
text_signature_string.map(|s| (Cow::Borrowed(&python_name), s)),
);
let krate = get_pyo3_crate(&krate);
@ -480,3 +480,26 @@ fn type_is_pymodule(ty: &syn::Type) -> bool {
}
false
}
/// Helper to get a text signature string, or None if unset or disabled
pub(crate) fn text_signature_or_none(
text_signature: Option<&TextSignatureAttribute>,
) -> Option<String> {
match text_signature.map(|attr| &attr.value) {
Some(TextSignatureAttributeValue::Str(s)) => Some(s.value()),
Some(TextSignatureAttributeValue::Disabled(_)) | None => None,
}
}
/// Helper to get a text signature string, using automatic generation if unset, or None if disabled
pub(crate) fn text_signature_or_auto(
text_signature: Option<&TextSignatureAttribute>,
signature: &FunctionSignature<'_>,
fn_type: &FnType,
) -> Option<String> {
match text_signature.map(|attr| &attr.value) {
Some(TextSignatureAttributeValue::Str(s)) => Some(s.value()),
None => Some(signature.text_signature(fn_type)),
Some(TextSignatureAttributeValue::Disabled(_)) => None,
}
}

View File

@ -12,7 +12,7 @@ use syn::{
use crate::{
attributes::{kw, KeywordAttribute},
method::FnArg,
method::{FnArg, FnType},
pyfunction::Argument,
};
@ -205,18 +205,18 @@ pub struct PythonSignature {
pub positional_parameters: Vec<String>,
pub positional_only_parameters: usize,
pub required_positional_parameters: usize,
pub accepts_varargs: bool,
pub varargs: Option<String>,
// Tuples of keyword name and whether it is required
pub keyword_only_parameters: Vec<(String, bool)>,
pub accepts_kwargs: bool,
pub kwargs: Option<String>,
}
impl PythonSignature {
pub fn has_no_args(&self) -> bool {
self.positional_parameters.is_empty()
&& self.keyword_only_parameters.is_empty()
&& !self.accepts_varargs
&& !self.accepts_kwargs
&& self.varargs.is_none()
&& self.kwargs.is_none()
}
}
@ -232,9 +232,9 @@ pub enum ParseState {
/// Accepting positional parameters after '/'
PositionalAfterPosargs,
/// Accepting keyword-only parameters after '*' or '*args'
Keywords(Option<String>),
Keywords,
/// After `**kwargs` nothing is allowed
Done(String),
Done,
}
impl ParseState {
@ -257,12 +257,12 @@ impl ParseState {
}
Ok(())
}
ParseState::Keywords(_) => {
ParseState::Keywords => {
signature.keyword_only_parameters.push((name, required));
Ok(())
}
ParseState::Done(s) => {
bail_spanned!(span => format!("no more arguments are allowed after `**{}`", s))
ParseState::Done => {
bail_spanned!(span => format!("no more arguments are allowed after `**{}`", signature.kwargs.as_deref().unwrap_or("")))
}
}
}
@ -274,15 +274,15 @@ impl ParseState {
) -> syn::Result<()> {
match self {
ParseState::Positional | ParseState::PositionalAfterPosargs => {
signature.accepts_varargs = true;
*self = ParseState::Keywords(Some(varargs.ident.to_string()));
signature.varargs = Some(varargs.ident.to_string());
*self = ParseState::Keywords;
Ok(())
}
ParseState::Keywords(s) => {
bail_spanned!(varargs.span() => format!("`*{}` not allowed after `*{}`", varargs.ident, s.as_deref().unwrap_or("")))
ParseState::Keywords => {
bail_spanned!(varargs.span() => format!("`*{}` not allowed after `*{}`", varargs.ident, signature.varargs.as_deref().unwrap_or("")))
}
ParseState::Done(s) => {
bail_spanned!(varargs.span() => format!("`*{}` not allowed after `**{}`", varargs.ident, s))
ParseState::Done => {
bail_spanned!(varargs.span() => format!("`*{}` not allowed after `**{}`", varargs.ident, signature.kwargs.as_deref().unwrap_or("")))
}
}
}
@ -293,15 +293,13 @@ impl ParseState {
kwargs: &SignatureItemKwargs,
) -> syn::Result<()> {
match self {
ParseState::Positional
| ParseState::PositionalAfterPosargs
| ParseState::Keywords(_) => {
signature.accepts_kwargs = true;
*self = ParseState::Done(kwargs.ident.to_string());
ParseState::Positional | ParseState::PositionalAfterPosargs | ParseState::Keywords => {
signature.kwargs = Some(kwargs.ident.to_string());
*self = ParseState::Done;
Ok(())
}
ParseState::Done(s) => {
bail_spanned!(kwargs.span() => format!("`**{}` not allowed after `**{}`", kwargs.ident, s))
ParseState::Done => {
bail_spanned!(kwargs.span() => format!("`**{}` not allowed after `**{}`", kwargs.ident, signature.kwargs.as_deref().unwrap_or("")))
}
}
}
@ -320,26 +318,26 @@ impl ParseState {
ParseState::PositionalAfterPosargs => {
bail_spanned!(span => "`/` not allowed after `/`")
}
ParseState::Keywords(s) => {
bail_spanned!(span => format!("`/` not allowed after `*{}`", s.as_deref().unwrap_or("")))
ParseState::Keywords => {
bail_spanned!(span => format!("`/` not allowed after `*{}`", signature.varargs.as_deref().unwrap_or("")))
}
ParseState::Done(s) => {
bail_spanned!(span => format!("`/` not allowed after `**{}`", s))
ParseState::Done => {
bail_spanned!(span => format!("`/` not allowed after `**{}`", signature.kwargs.as_deref().unwrap_or("")))
}
}
}
fn finish_pos_args(&mut self, span: Span) -> syn::Result<()> {
fn finish_pos_args(&mut self, signature: &PythonSignature, span: Span) -> syn::Result<()> {
match self {
ParseState::Positional | ParseState::PositionalAfterPosargs => {
*self = ParseState::Keywords(None);
*self = ParseState::Keywords;
Ok(())
}
ParseState::Keywords(s) => {
bail_spanned!(span => format!("`*` not allowed after `*{}`", s.as_deref().unwrap_or("")))
ParseState::Keywords => {
bail_spanned!(span => format!("`*` not allowed after `*{}`", signature.varargs.as_deref().unwrap_or("")))
}
ParseState::Done(s) => {
bail_spanned!(span => format!("`*` not allowed after `**{}`", s))
ParseState::Done => {
bail_spanned!(span => format!("`*` not allowed after `**{}`", signature.kwargs.as_deref().unwrap_or("")))
}
}
}
@ -386,7 +384,9 @@ impl<'a> FunctionSignature<'a> {
fn_arg.default = Some(default.clone());
}
}
SignatureItem::VarargsSep(sep) => parse_state.finish_pos_args(sep.span())?,
SignatureItem::VarargsSep(sep) => {
parse_state.finish_pos_args(&python_signature, sep.span())?
}
SignatureItem::Varargs(varargs) => {
let fn_arg = next_argument_checked(&varargs.ident)?;
fn_arg.is_varargs = true;
@ -423,8 +423,8 @@ impl<'a> FunctionSignature<'a> {
mut arguments: Vec<FnArg<'a>>,
deprecated_args: DeprecatedArgs,
) -> syn::Result<Self> {
let mut accepts_varargs = false;
let mut accepts_kwargs = false;
let mut varargs = None;
let mut kwargs = None;
let mut keyword_only_parameters = Vec::new();
fn first_n_argument_names(arguments: &[FnArg<'_>], count: usize) -> Vec<String> {
@ -481,13 +481,21 @@ impl<'a> FunctionSignature<'a> {
}
Argument::PosOnlyArgsSeparator => {}
Argument::VarArgsSeparator => {}
Argument::VarArgs(_) => {
Argument::VarArgs(path) => {
fn_arg.is_varargs = true;
accepts_varargs = true;
if let Some(ident) = path.get_ident() {
varargs = Some(ident.to_string());
} else {
bail_spanned!(path.span() => "expected ident for *args");
};
}
Argument::KeywordArgs(_) => {
Argument::KeywordArgs(path) => {
fn_arg.is_kwargs = true;
accepts_kwargs = true;
if let Some(ident) = path.get_ident() {
kwargs = Some(ident.to_string());
} else {
bail_spanned!(path.span() => "expected ident for **kwargs");
};
}
}
} else {
@ -513,9 +521,9 @@ impl<'a> FunctionSignature<'a> {
positional_parameters,
positional_only_parameters,
required_positional_parameters,
accepts_varargs,
varargs,
keyword_only_parameters,
accepts_kwargs,
kwargs,
},
attribute: None,
})
@ -556,4 +564,80 @@ impl<'a> FunctionSignature<'a> {
attribute: None,
}
}
pub fn text_signature(&self, fn_type: &FnType) -> String {
// automatic text signature generation
let self_argument = match fn_type {
FnType::FnNew | FnType::Getter(_) | FnType::Setter(_) | FnType::ClassAttribute => {
unreachable!()
}
FnType::Fn(_) => Some("self"),
FnType::FnModule => Some("module"),
FnType::FnClass => Some("cls"),
FnType::FnStatic => None,
};
let mut output = String::new();
output.push('(');
if let Some(arg) = self_argument {
output.push('$');
output.push_str(arg);
}
let mut maybe_push_comma = {
let mut first = self_argument.is_none();
move |output: &mut String| {
if !first {
output.push_str(", ");
} else {
first = false;
}
}
};
let py_sig = &self.python_signature;
for (i, parameter) in py_sig.positional_parameters.iter().enumerate() {
maybe_push_comma(&mut output);
output.push_str(parameter);
if i >= py_sig.required_positional_parameters {
// has a default, just use ... for now
output.push_str("=...");
}
if py_sig.positional_only_parameters > 0 && i + 1 == py_sig.positional_only_parameters {
output.push_str(", /")
}
}
if let Some(varargs) = &py_sig.varargs {
maybe_push_comma(&mut output);
output.push('*');
output.push_str(varargs);
} else if !py_sig.keyword_only_parameters.is_empty() {
maybe_push_comma(&mut output);
output.push('*');
}
for (parameter, required) in &py_sig.keyword_only_parameters {
maybe_push_comma(&mut output);
output.push_str(parameter);
if !required {
// has a default, just use ... for now
output.push_str("=...")
}
}
if let Some(kwargs) = &py_sig.kwargs {
maybe_push_comma(&mut output);
output.push_str("**");
output.push_str(kwargs);
}
output.push(')');
output
}
}

View File

@ -5,7 +5,7 @@ use proc_macro2::{Span, TokenStream};
use quote::ToTokens;
use syn::{spanned::Spanned, Ident};
use crate::attributes::{CrateAttribute, TextSignatureAttribute};
use crate::attributes::CrateAttribute;
/// Macro inspired by `anyhow::anyhow!` to create a compiler error with the given span.
macro_rules! err_spanned {
@ -68,7 +68,7 @@ pub struct PythonDoc(TokenStream);
/// e.g. concat!("...", "\n", "\0")
pub fn get_doc(
attrs: &[syn::Attribute],
text_signature: Option<(Cow<'_, Ident>, &TextSignatureAttribute)>,
text_signature: Option<(Cow<'_, Ident>, String)>,
) -> PythonDoc {
let mut tokens = TokenStream::new();
let comma = syn::token::Comma(Span::call_site());
@ -79,8 +79,7 @@ pub fn get_doc(
syn::token::Bracket(Span::call_site()).surround(&mut tokens, |tokens| {
if let Some((python_name, text_signature)) = text_signature {
// create special doc string lines to set `__text_signature__`
let signature_lines =
format!("{}{}\n--\n\n", python_name, text_signature.value.value());
let signature_lines = format!("{}{}\n--\n\n", python_name, text_signature);
signature_lines.to_tokens(tokens);
comma.to_tokens(tokens);
}

View File

@ -142,7 +142,7 @@ impl PyList {
/// # Safety
///
/// Caller must verify that the index is within the bounds of the list.
#[cfg(not(any(Py_LIMITED_API, PyPy)))]
#[cfg(not(Py_LIMITED_API))]
pub unsafe fn get_item_unchecked(&self, index: usize) -> &PyAny {
let item = ffi::PyList_GET_ITEM(self.as_ptr(), index as Py_ssize_t);
// PyList_GET_ITEM return borrowed ptr; must make owned for safety (see #890).

View File

@ -1,6 +1,7 @@
#![cfg(feature = "macros")]
use pyo3::prelude::*;
use pyo3::types::{PyDict, PyTuple};
use pyo3::{types::PyType, wrap_pymodule, PyCell};
mod common;
@ -117,6 +118,179 @@ fn test_function() {
});
}
#[test]
fn test_auto_test_signature_function() {
#[pyfunction]
fn my_function(a: i32, b: Option<i32>, c: i32) {
let _ = (a, b, c);
}
#[pyfunction(pass_module)]
fn my_function_2(module: &PyModule, a: i32, b: Option<i32>, c: i32) {
let _ = (module, a, b, c);
}
#[pyfunction(signature = (a, /, b = None, *, c = 5))]
fn my_function_3(a: i32, b: Option<i32>, c: i32) {
let _ = (a, b, c);
}
#[pyfunction(signature = (a, /, b = None, *args, c, d=5, **kwargs))]
fn my_function_4(
a: i32,
b: Option<i32>,
args: &PyTuple,
c: i32,
d: i32,
kwargs: Option<&PyDict>,
) {
let _ = (a, b, args, c, d, kwargs);
}
Python::with_gil(|py| {
let f = wrap_pyfunction!(my_function)(py).unwrap();
py_assert!(py, f, "f.__text_signature__ == '(a, b, c)'");
let f = wrap_pyfunction!(my_function_2)(py).unwrap();
py_assert!(py, f, "f.__text_signature__ == '($module, a, b, c)'");
let f = wrap_pyfunction!(my_function_3)(py).unwrap();
py_assert!(py, f, "f.__text_signature__ == '(a, /, b=..., *, c=...)'");
let f = wrap_pyfunction!(my_function_4)(py).unwrap();
py_assert!(
py,
f,
"f.__text_signature__ == '(a, /, b=..., *args, c, d=..., **kwargs)'"
);
});
}
#[test]
fn test_auto_test_signature_method() {
#[pyclass]
struct MyClass {}
#[pymethods]
impl MyClass {
fn method(&self, a: i32, b: Option<i32>, c: i32) {
let _ = (a, b, c);
}
#[pyo3(signature = (a, /, b = None, *, c = 5))]
fn method_2(&self, a: i32, b: Option<i32>, c: i32) {
let _ = (a, b, c);
}
#[pyo3(signature = (a, /, b = None, *args, c, d=5, **kwargs))]
fn method_3(
&self,
a: i32,
b: Option<i32>,
args: &PyTuple,
c: i32,
d: i32,
kwargs: Option<&PyDict>,
) {
let _ = (a, b, args, c, d, kwargs);
}
#[staticmethod]
fn staticmethod(a: i32, b: Option<i32>, c: i32) {
let _ = (a, b, c);
}
#[classmethod]
fn classmethod(cls: &PyType, a: i32, b: Option<i32>, c: i32) {
let _ = (cls, a, b, c);
}
}
Python::with_gil(|py| {
let cls = py.get_type::<MyClass>();
py_assert!(
py,
cls,
"cls.method.__text_signature__ == '($self, a, b, c)'"
);
py_assert!(
py,
cls,
"cls.method_2.__text_signature__ == '($self, a, /, b=..., *, c=...)'"
);
py_assert!(
py,
cls,
"cls.method_3.__text_signature__ == '($self, a, /, b=..., *args, c, d=..., **kwargs)'"
);
py_assert!(
py,
cls,
"cls.staticmethod.__text_signature__ == '(a, b, c)'"
);
py_assert!(
py,
cls,
"cls.classmethod.__text_signature__ == '($cls, a, b, c)'"
);
});
}
#[test]
fn test_auto_test_signature_opt_out() {
#[pyfunction(text_signature = None)]
fn my_function(a: i32, b: Option<i32>, c: i32) {
let _ = (a, b, c);
}
#[pyfunction(signature = (a, /, b = None, *, c = 5), text_signature = None)]
fn my_function_2(a: i32, b: Option<i32>, c: i32) {
let _ = (a, b, c);
}
#[pyclass]
struct MyClass {}
#[pymethods]
impl MyClass {
#[pyo3(text_signature = None)]
fn method(&self, a: i32, b: Option<i32>, c: i32) {
let _ = (a, b, c);
}
#[pyo3(signature = (a, /, b = None, *, c = 5), text_signature = None)]
fn method_2(&self, a: i32, b: Option<i32>, c: i32) {
let _ = (a, b, c);
}
#[staticmethod]
#[pyo3(text_signature = None)]
fn staticmethod(a: i32, b: Option<i32>, c: i32) {
let _ = (a, b, c);
}
#[classmethod]
#[pyo3(text_signature = None)]
fn classmethod(cls: &PyType, a: i32, b: Option<i32>, c: i32) {
let _ = (cls, a, b, c);
}
}
Python::with_gil(|py| {
let f = wrap_pyfunction!(my_function)(py).unwrap();
py_assert!(py, f, "f.__text_signature__ == None");
let f = wrap_pyfunction!(my_function_2)(py).unwrap();
py_assert!(py, f, "f.__text_signature__ == None");
let cls = py.get_type::<MyClass>();
py_assert!(py, cls, "cls.method.__text_signature__ == None");
py_assert!(py, cls, "cls.method_2.__text_signature__ == None");
py_assert!(py, cls, "cls.staticmethod.__text_signature__ == None");
py_assert!(py, cls, "cls.classmethod.__text_signature__ == None");
});
}
#[test]
fn test_pyfn() {
#[pymodule]

View File

@ -79,6 +79,19 @@ impl MyClass {
fn text_signature_on_classattr() {}
}
#[pymethods]
impl MyClass {
#[pyo3(text_signature = 1)]
fn invalid_text_signature() {}
}
#[pymethods]
impl MyClass {
#[pyo3(text_signature = "()")]
#[pyo3(text_signature = None)]
fn duplicate_text_signature() {}
}
#[pymethods]
impl MyClass {
#[getter(x)]

View File

@ -64,73 +64,85 @@ error: `text_signature` not allowed with `classattr`
78 | #[pyo3(text_signature = "()")]
| ^^^^^^^^^^^^^^
error: `signature` not allowed with `getter`
--> tests/ui/invalid_pymethods.rs:85:12
error: expected a string literal or `None`
--> tests/ui/invalid_pymethods.rs:84:30
|
85 | #[pyo3(signature = ())]
84 | #[pyo3(text_signature = 1)]
| ^
error: `text_signature` may only be specified once
--> tests/ui/invalid_pymethods.rs:91:12
|
91 | #[pyo3(text_signature = None)]
| ^^^^^^^^^^^^^^
error: `signature` not allowed with `getter`
--> tests/ui/invalid_pymethods.rs:98:12
|
98 | #[pyo3(signature = ())]
| ^^^^^^^^^
error: `signature` not allowed with `setter`
--> tests/ui/invalid_pymethods.rs:92:12
|
92 | #[pyo3(signature = ())]
| ^^^^^^^^^
--> tests/ui/invalid_pymethods.rs:105:12
|
105 | #[pyo3(signature = ())]
| ^^^^^^^^^
error: `signature` not allowed with `classattr`
--> tests/ui/invalid_pymethods.rs:99:12
|
99 | #[pyo3(signature = ())]
| ^^^^^^^^^
--> tests/ui/invalid_pymethods.rs:112:12
|
112 | #[pyo3(signature = ())]
| ^^^^^^^^^
error: cannot specify a second method type
--> tests/ui/invalid_pymethods.rs:106:7
--> tests/ui/invalid_pymethods.rs:119:7
|
106 | #[staticmethod]
119 | #[staticmethod]
| ^^^^^^^^^^^^
error: Python functions cannot have generic type parameters
--> tests/ui/invalid_pymethods.rs:112:23
--> tests/ui/invalid_pymethods.rs:125:23
|
112 | fn generic_method<T>(value: T) {}
125 | fn generic_method<T>(value: T) {}
| ^
error: Python functions cannot have `impl Trait` arguments
--> tests/ui/invalid_pymethods.rs:117:48
--> tests/ui/invalid_pymethods.rs:130:48
|
117 | fn impl_trait_method_first_arg(impl_trait: impl AsRef<PyAny>) {}
130 | fn impl_trait_method_first_arg(impl_trait: impl AsRef<PyAny>) {}
| ^^^^
error: Python functions cannot have `impl Trait` arguments
--> tests/ui/invalid_pymethods.rs:122:56
--> tests/ui/invalid_pymethods.rs:135:56
|
122 | fn impl_trait_method_second_arg(&self, impl_trait: impl AsRef<PyAny>) {}
135 | fn impl_trait_method_second_arg(&self, impl_trait: impl AsRef<PyAny>) {}
| ^^^^
error: `async fn` is not yet supported for Python functions.
Additional crates such as `pyo3-asyncio` can be used to integrate async Rust and Python. For more information, see https://github.com/PyO3/pyo3/issues/1632
--> tests/ui/invalid_pymethods.rs:127:5
--> tests/ui/invalid_pymethods.rs:140:5
|
127 | async fn async_method(&self) {}
140 | async fn async_method(&self) {}
| ^^^^^
error: `pass_module` cannot be used on Python methods
--> tests/ui/invalid_pymethods.rs:132:12
--> tests/ui/invalid_pymethods.rs:145:12
|
132 | #[pyo3(pass_module)]
145 | #[pyo3(pass_module)]
| ^^^^^^^^^^^
error: Python objects are shared, so 'self' cannot be moved out of the Python interpreter.
Try `&self`, `&mut self, `slf: PyRef<'_, Self>` or `slf: PyRefMut<'_, Self>`.
--> tests/ui/invalid_pymethods.rs:138:29
--> tests/ui/invalid_pymethods.rs:151:29
|
138 | fn method_self_by_value(self) {}
151 | fn method_self_by_value(self) {}
| ^^^^
error[E0592]: duplicate definitions with name `__pymethod___new____`
--> tests/ui/invalid_pymethods.rs:143:1
--> tests/ui/invalid_pymethods.rs:156:1
|
143 | #[pymethods]
156 | #[pymethods]
| ^^^^^^^^^^^^
| |
| duplicate definitions for `__pymethod___new____`
@ -139,9 +151,9 @@ error[E0592]: duplicate definitions with name `__pymethod___new____`
= note: this error originates in the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info)
error[E0592]: duplicate definitions with name `__pymethod_func__`
--> tests/ui/invalid_pymethods.rs:158:1
--> tests/ui/invalid_pymethods.rs:171:1
|
158 | #[pymethods]
171 | #[pymethods]
| ^^^^^^^^^^^^
| |
| duplicate definitions for `__pymethod_func__`