diff --git a/guide/src/function.md b/guide/src/function.md
index b0d6c0b0..fe74e5df 100644
--- a/guide/src/function.md
+++ b/guide/src/function.md
@@ -73,39 +73,7 @@ The `#[pyo3]` attribute can be used to modify properties of the generated Python
- `#[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).
- `#[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`]
diff --git a/guide/src/function/signature.md b/guide/src/function/signature.md
index 39e73308..651b4a3f 100644
--- a/guide/src/function/signature.md
+++ b/guide/src/function/signature.md
@@ -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 = "()")]` 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
+```
diff --git a/newsfragments/2784.changed.md b/newsfragments/2784.changed.md
new file mode 100644
index 00000000..74c0428d
--- /dev/null
+++ b/newsfragments/2784.changed.md
@@ -0,0 +1 @@
+Automatically generate `__text_signature__` for all Python functions created using `#[pyfunction]` and `#[pymethods]`.
diff --git a/newsfragments/2827.added.md b/newsfragments/2827.added.md
new file mode 100644
index 00000000..4bd084ae
--- /dev/null
+++ b/newsfragments/2827.added.md
@@ -0,0 +1 @@
+Add `PyList::get_item_unchecked` for PyPy.
diff --git a/pyo3-macros-backend/src/attributes.rs b/pyo3-macros-backend/src/attributes.rs
index 6235c7e1..247463ca 100644
--- a/pyo3-macros-backend/src/attributes.rs
+++ b/pyo3-macros-backend/src/attributes.rs
@@ -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 {
+ if let Ok(lit_str) = input.parse::() {
+ return Ok(TextSignatureAttributeValue::Str(lit_str));
+ }
+
+ let err_span = match input.parse::() {
+ 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;
pub type FreelistAttribute = KeywordAttribute>;
pub type ModuleAttribute = KeywordAttribute;
pub type NameAttribute = KeywordAttribute;
-pub type TextSignatureAttribute = KeywordAttribute;
+pub type TextSignatureAttribute = KeywordAttribute;
impl Parse for KeywordAttribute {
fn parse(input: ParseStream<'_>) -> Result {
diff --git a/pyo3-macros-backend/src/method.rs b/pyo3-macros-backend/src/method.rs
index 33d585e4..b79bb34b 100644
--- a/pyo3-macros-backend/src/method.rs
+++ b/pyo3-macros-backend/src/method.rs
@@ -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));
diff --git a/pyo3-macros-backend/src/params.rs b/pyo3-macros-backend/src/params.rs
index a069d559..22d9d3e8 100644
--- a/pyo3-macros-backend/src/params.rs
+++ b/pyo3-macros-backend/src/params.rs
@@ -82,12 +82,12 @@ pub fn impl_arg_params(
.map(|arg| impl_arg_param(arg, &mut option_pos, py, &args_array))
.collect::>()?;
- 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 }
diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs
index e15900b1..bfc9a5f1 100644
--- a/pyo3-macros-backend/src/pyclass.rs
+++ b/pyo3-macros-backend/src/pyclass.rs
@@ -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 {
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)
diff --git a/pyo3-macros-backend/src/pyfunction.rs b/pyo3-macros-backend/src/pyfunction.rs
index 5e401818..6ce47409 100644
--- a/pyo3-macros-backend/src/pyfunction.rs
+++ b/pyo3-macros-backend/src/pyfunction.rs
@@ -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 {
+ 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 {
+ 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,
+ }
+}
diff --git a/pyo3-macros-backend/src/pyfunction/signature.rs b/pyo3-macros-backend/src/pyfunction/signature.rs
index 516d8031..0e4e3b2b 100644
--- a/pyo3-macros-backend/src/pyfunction/signature.rs
+++ b/pyo3-macros-backend/src/pyfunction/signature.rs
@@ -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,
pub positional_only_parameters: usize,
pub required_positional_parameters: usize,
- pub accepts_varargs: bool,
+ pub varargs: Option,
// Tuples of keyword name and whether it is required
pub keyword_only_parameters: Vec<(String, bool)>,
- pub accepts_kwargs: bool,
+ pub kwargs: Option,
}
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),
+ 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>,
deprecated_args: DeprecatedArgs,
) -> syn::Result {
- 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 {
@@ -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
+ }
}
diff --git a/pyo3-macros-backend/src/utils.rs b/pyo3-macros-backend/src/utils.rs
index cbbe2b31..ad20e917 100644
--- a/pyo3-macros-backend/src/utils.rs
+++ b/pyo3-macros-backend/src/utils.rs
@@ -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);
}
diff --git a/src/types/list.rs b/src/types/list.rs
index d81e8153..2ef7bca8 100644
--- a/src/types/list.rs
+++ b/src/types/list.rs
@@ -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).
diff --git a/tests/test_text_signature.rs b/tests/test_text_signature.rs
index 51b69cce..63ed8161 100644
--- a/tests/test_text_signature.rs
+++ b/tests/test_text_signature.rs
@@ -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, c: i32) {
+ let _ = (a, b, c);
+ }
+
+ #[pyfunction(pass_module)]
+ fn my_function_2(module: &PyModule, a: i32, b: Option, c: i32) {
+ let _ = (module, a, b, c);
+ }
+
+ #[pyfunction(signature = (a, /, b = None, *, c = 5))]
+ fn my_function_3(a: i32, b: Option, c: i32) {
+ let _ = (a, b, c);
+ }
+
+ #[pyfunction(signature = (a, /, b = None, *args, c, d=5, **kwargs))]
+ fn my_function_4(
+ a: i32,
+ b: Option,
+ 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, c: i32) {
+ let _ = (a, b, c);
+ }
+
+ #[pyo3(signature = (a, /, b = None, *, c = 5))]
+ fn method_2(&self, a: i32, b: Option, c: i32) {
+ let _ = (a, b, c);
+ }
+
+ #[pyo3(signature = (a, /, b = None, *args, c, d=5, **kwargs))]
+ fn method_3(
+ &self,
+ a: i32,
+ b: Option,
+ args: &PyTuple,
+ c: i32,
+ d: i32,
+ kwargs: Option<&PyDict>,
+ ) {
+ let _ = (a, b, args, c, d, kwargs);
+ }
+
+ #[staticmethod]
+ fn staticmethod(a: i32, b: Option, c: i32) {
+ let _ = (a, b, c);
+ }
+
+ #[classmethod]
+ fn classmethod(cls: &PyType, a: i32, b: Option, c: i32) {
+ let _ = (cls, a, b, c);
+ }
+ }
+
+ Python::with_gil(|py| {
+ let cls = py.get_type::();
+ 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, c: i32) {
+ let _ = (a, b, c);
+ }
+
+ #[pyfunction(signature = (a, /, b = None, *, c = 5), text_signature = None)]
+ fn my_function_2(a: i32, b: Option, c: i32) {
+ let _ = (a, b, c);
+ }
+
+ #[pyclass]
+ struct MyClass {}
+
+ #[pymethods]
+ impl MyClass {
+ #[pyo3(text_signature = None)]
+ fn method(&self, a: i32, b: Option, c: i32) {
+ let _ = (a, b, c);
+ }
+
+ #[pyo3(signature = (a, /, b = None, *, c = 5), text_signature = None)]
+ fn method_2(&self, a: i32, b: Option, c: i32) {
+ let _ = (a, b, c);
+ }
+
+ #[staticmethod]
+ #[pyo3(text_signature = None)]
+ fn staticmethod(a: i32, b: Option, c: i32) {
+ let _ = (a, b, c);
+ }
+
+ #[classmethod]
+ #[pyo3(text_signature = None)]
+ fn classmethod(cls: &PyType, a: i32, b: Option, 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::();
+ 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]
diff --git a/tests/ui/invalid_pymethods.rs b/tests/ui/invalid_pymethods.rs
index 26cd2bf6..a6690b05 100644
--- a/tests/ui/invalid_pymethods.rs
+++ b/tests/ui/invalid_pymethods.rs
@@ -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)]
diff --git a/tests/ui/invalid_pymethods.stderr b/tests/ui/invalid_pymethods.stderr
index 36eab673..e660b52c 100644
--- a/tests/ui/invalid_pymethods.stderr
+++ b/tests/ui/invalid_pymethods.stderr
@@ -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(value: T) {}
+125 | fn generic_method(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) {}
+130 | fn impl_trait_method_first_arg(impl_trait: impl AsRef) {}
| ^^^^
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) {}
+135 | fn impl_trait_method_second_arg(&self, impl_trait: impl AsRef) {}
| ^^^^
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__`