Skip to content

Commit

Permalink
add #[pyo3(signature = (...))] attribute (#2702)
Browse files Browse the repository at this point in the history
  • Loading branch information
davidhewitt authored Oct 25, 2022
1 parent 747d791 commit 8e8b484
Show file tree
Hide file tree
Showing 37 changed files with 1,775 additions and 442 deletions.
2 changes: 1 addition & 1 deletion examples/decorator/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ impl PyCounter {
self.count.get()
}

#[args(args = "*", kwargs = "**")]
#[pyo3(signature = (*args, **kwargs))]
fn __call__(
&self,
py: Python<'_>,
Expand Down
19 changes: 7 additions & 12 deletions guide/src/class.md
Original file line number Diff line number Diff line change
Expand Up @@ -632,9 +632,9 @@ impl MyClass {

## Method arguments

Similar to `#[pyfunction]`, the `#[args]` attribute can be used to specify the way that `#[pymethods]` accept arguments. Consult the documentation for [`function signatures`](./function/signature.md) to see the parameters this attribute accepts.
Similar to `#[pyfunction]`, the `#[pyo3(signature = (...))]` attribute can be used to specify the way that `#[pymethods]` accept arguments. Consult the documentation for [`function signatures`](./function/signature.md) to see the parameters this attribute accepts.

The following example defines a class `MyClass` with a method `method`. This method has an `#[args]` attribute which sets default values for `num` and `name`, and indicates that `py_args` should collect all extra positional arguments and `py_kwargs` all extra keyword arguments:
The following example defines a class `MyClass` with a method `method`. This method has a signature which sets default values for `num` and `name`, and indicates that `py_args` should collect all extra positional arguments and `py_kwargs` all extra keyword arguments:

```rust
# use pyo3::prelude::*;
Expand All @@ -647,29 +647,24 @@ use pyo3::types::{PyDict, PyTuple};
#[pymethods]
impl MyClass {
#[new]
#[args(num = "-1")]
#[pyo3(signature = (num=-1))]
fn new(num: i32) -> Self {
MyClass { num }
}

#[args(
num = "10",
py_args = "*",
name = "\"Hello\"",
py_kwargs = "**"
)]
#[pyo3(signature = (num=10, *py_args, name="Hello", **py_kwargs))]
fn method(
&mut self,
num: i32,
name: &str,
py_args: &PyTuple,
name: &str,
py_kwargs: Option<&PyDict>,
) -> String {
let num_before = self.num;
self.num = num;
format!(
"py_args={:?}, py_kwargs={:?}, name={}, num={} num_before={}",
py_args, py_kwargs, name, self.num, num_before,
"num={} (was previously={}), py_args={:?}, name={}, py_kwargs={:?} ",
num, num_before, py_args, name, py_kwargs,
)
}
}
Expand Down
6 changes: 3 additions & 3 deletions guide/src/class/call.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ def Counter(wraps):
A [previous implementation] used a normal `u64`, which meant it required a `&mut self` receiver to update the count:

```rust,ignore
#[args(args = "*", kwargs = "**")]
#[pyo3(signature = (*args, **kwargs))]
fn __call__(&mut self, py: Python<'_>, args: &PyTuple, kwargs: Option<&PyDict>) -> PyResult<Py<PyAny>> {
self.count += 1;
let name = self.wraps.getattr(py, "__name__")?;
Expand All @@ -98,7 +98,7 @@ As a result, something innocent like this will raise an exception:
def say_hello():
if say_hello.count < 2:
print(f"hello from decorator")

say_hello()
# RuntimeError: Already borrowed
```
Expand All @@ -113,4 +113,4 @@ This shows the dangers of running arbitrary Python code - note that "running arb
This is especially important if you are writing unsafe code; Python code must never be able to cause undefined behavior. You must ensure that your Rust code is in a consistent state before doing any of the above things.

[previous implementation]: https://github.com/PyO3/pyo3/discussions/2598 "Thread Safe Decorator <Help Wanted> · Discussion #2598 · PyO3/pyo3"
[`Cell`]: https://doc.rust-lang.org/std/cell/struct.Cell.html "Cell in std::cell - Rust"
[`Cell`]: https://doc.rust-lang.org/std/cell/struct.Cell.html "Cell in std::cell - Rust"
5 changes: 5 additions & 0 deletions guide/src/function.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ This chapter of the guide explains full usage of the `#[pyfunction]` attribute.

- [Function options](#function-options)
- [`#[pyo3(name = "...")]`](#name)
- [`#[pyo3(signature = (...))]`](#signature)
- [`#[pyo3(text_signature = "...")]`](#text_signature)
- [`#[pyo3(pass_module)]`](#pass_module)
- [Per-argument options](#per-argument-options)
Expand Down Expand Up @@ -64,6 +65,10 @@ The `#[pyo3]` attribute can be used to modify properties of the generated Python
# });
```

- <a name="signature"></a> `#[pyo3(signature = (...))]`

Defines the function signature in Python. See [Function Signatures](./function/signature.md).

- <a name="text_signature"></a> `#[pyo3(text_signature = "...")]`

Sets the function signature visible in Python tooling (such as via [`inspect.signature`]).
Expand Down
144 changes: 121 additions & 23 deletions guide/src/function/signature.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,121 @@

The `#[pyfunction]` attribute also accepts parameters to control how the generated Python function accepts arguments. Just like in Python, arguments can be positional-only, keyword-only, or accept either. `*args` lists and `**kwargs` dicts can also be accepted. These parameters also work for `#[pymethods]` which will be introduced in the [Python Classes](../class.md) section of the guide.

Like Python, by default PyO3 accepts all arguments as either positional or keyword arguments. The extra arguments to `#[pyfunction]` modify this behaviour. For example, below is a function that accepts arbitrary keyword arguments (`**kwargs` in Python syntax) and returns the number that was passed:
Like Python, by default PyO3 accepts all arguments as either positional or keyword arguments. There are two ways to modify this behaviour:
- The `#[pyo3(signature = (...))]` option which allows writing a signature in Python syntax.
- Extra arguments directly to `#[pyfunction]`. (See deprecated form)

## Using `#[pyo3(signature = (...))]`

For example, below is a function that accepts arbitrary keyword arguments (`**kwargs` in Python syntax) and returns the number that was passed:

```rust
use pyo3::prelude::*;
use pyo3::types::PyDict;

#[pyfunction]
#[pyo3(signature = (**kwds))]
fn num_kwds(kwds: Option<&PyDict>) -> usize {
kwds.map_or(0, |dict| dict.len())
}

#[pymodule]
fn module_with_functions(py: Python<'_>, m: &PyModule) -> PyResult<()> {
m.add_function(wrap_pyfunction!(num_kwds, m)?).unwrap();
Ok(())
}
```

Just like in Python, the following constructs can be part of the signature::

* `/`: positional-only arguments separator, each parameter defined before `/` is a positional-only parameter.
* `*`: var arguments separator, each parameter defined after `*` is a keyword-only parameter.
* `*args`: "args" is var args. Type of the `args` parameter has to be `&PyTuple`.
* `**kwargs`: "kwargs" receives keyword arguments. The type of the `kwargs` parameter has to be `Option<&PyDict>`.
* `arg=Value`: arguments with default value.
If the `arg` argument is defined after var arguments, it is treated as a keyword-only argument.
Note that `Value` has to be valid rust code, PyO3 just inserts it into the generated
code unmodified.

Example:
```rust
# use pyo3::prelude::*;
use pyo3::types::{PyDict, PyTuple};
#
# #[pyclass]
# struct MyClass {
# num: i32,
# }
#[pymethods]
impl MyClass {
#[new]
#[pyo3(signature = (num=-1))]
fn new(num: i32) -> Self {
MyClass { num }
}

#[pyo3(signature = (num=10, *py_args, name="Hello", **py_kwargs))]
fn method(
&mut self,
num: i32,
py_args: &PyTuple,
name: &str,
py_kwargs: Option<&PyDict>,
) -> String {
let num_before = self.num;
self.num = num;
format!(
"num={} (was previously={}), py_args={:?}, name={}, py_kwargs={:?} ",
num, num_before, py_args, name, py_kwargs,
)
}

fn make_change(&mut self, num: i32) -> PyResult<String> {
self.num = num;
Ok(format!("num={}", self.num))
}
}
```
N.B. the position of the `/` and `*` arguments (if included) control the system of handling positional and keyword arguments. In Python:
```python
import mymodule

mc = mymodule.MyClass()
print(mc.method(44, False, "World", 666, x=44, y=55))
print(mc.method(num=-1, name="World"))
print(mc.make_change(44, False))
```
Produces output:
```text
py_args=('World', 666), py_kwargs=Some({'x': 44, 'y': 55}), name=Hello, num=44
py_args=(), py_kwargs=None, name=World, num=-1
num=44
num=-1
```

> Note: for keywords like `struct`, to use it as a function argument, use "raw ident" syntax `r#struct` in both the signature and the function definition:
>
> ```rust
> # #![allow(dead_code)]
> # use pyo3::prelude::*;
> #[pyfunction(signature = (r#struct = "foo"))]
> fn function_with_keyword(r#struct: &str) {
> # let _ = r#struct;
> /* ... */
> }
> ```
## Deprecated form
The `#[pyfunction]` macro can take the argument specification directly, but this method is deprecated in PyO3 0.18 because the `#[pyo3(signature)]` option offers a simpler syntax and better validation.
The `#[pymethods]` macro has an `#[args]` attribute which accepts the deprecated form.
Below are the same examples as above which using the deprecated syntax:
```rust
# #![allow(deprecated)]
use pyo3::prelude::*;
use pyo3::types::PyDict;
Expand Down Expand Up @@ -38,6 +150,7 @@ The following parameters can be passed to the `#[pyfunction]` attribute:

Example:
```rust
# #![allow(deprecated)]
# use pyo3::prelude::*;
use pyo3::types::{PyDict, PyTuple};
#
Expand All @@ -62,15 +175,16 @@ impl MyClass {
fn method(
&mut self,
num: i32,
name: &str,
py_args: &PyTuple,
name: &str,
py_kwargs: Option<&PyDict>,
) -> PyResult<String> {
) -> String {
let num_before = self.num;
self.num = num;
Ok(format!(
"py_args={:?}, py_kwargs={:?}, name={}, num={}",
py_args, py_kwargs, name, self.num
))
format!(
"num={} (was previously={}), py_args={:?}, name={}, py_kwargs={:?} ",
num, num_before, py_args, name, py_kwargs,
)
}

fn make_change(&mut self, num: i32) -> PyResult<String> {
Expand All @@ -79,19 +193,3 @@ impl MyClass {
}
}
```
N.B. the position of the `"/"` and `"*"` arguments (if included) control the system of handling positional and keyword arguments. In Python:
```python
import mymodule

mc = mymodule.MyClass()
print(mc.method(44, False, "World", 666, x=44, y=55))
print(mc.method(num=-1, name="World"))
print(mc.make_change(44, False))
```
Produces output:
```text
py_args=('World', 666), py_kwargs=Some({'x': 44, 'y': 55}), name=Hello, num=44
py_args=(), py_kwargs=None, name=World, num=-1
num=44
num=-1
```
1 change: 1 addition & 0 deletions newsfragments/2702.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add `#[pyo3(signature = (...))]` option for `#[pyfunction]` and `#[pymethods]`.
1 change: 1 addition & 0 deletions newsfragments/2702.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Deprecate `#[args]` attribute and passing "args" specification directly to `#[pyfunction]` in favour of the new `#[pyo3(signature = (...))]` option.
3 changes: 2 additions & 1 deletion pyo3-macros-backend/src/attributes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use syn::{
};

pub mod kw {
syn::custom_keyword!(args);
syn::custom_keyword!(annotation);
syn::custom_keyword!(attribute);
syn::custom_keyword!(dict);
Expand Down Expand Up @@ -42,7 +43,7 @@ pub struct KeywordAttribute<K, V> {
}

/// A helper type which parses the inner type via a literal string
/// e.g. LitStrValue<Path> -> parses "some::path" in quotes.
/// e.g. `LitStrValue<Path>` -> parses "some::path" in quotes.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct LitStrValue<T>(pub T);

Expand Down
6 changes: 6 additions & 0 deletions pyo3-macros-backend/src/deprecations.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
use proc_macro2::{Span, TokenStream};
use quote::{quote_spanned, ToTokens};

// Clippy complains all these variants have the same prefix "Py"...
#[allow(clippy::enum_variant_names)]
pub enum Deprecation {
PyClassGcOption,
PyFunctionArguments,
PyMethodArgsAttribute,
}

impl Deprecation {
fn ident(&self, span: Span) -> syn::Ident {
let string = match self {
Deprecation::PyClassGcOption => "PYCLASS_GC_OPTION",
Deprecation::PyFunctionArguments => "PYFUNCTION_ARGUMENTS",
Deprecation::PyMethodArgsAttribute => "PYMETHODS_ARGS_ATTRIBUTE",
};
syn::Ident::new(string, span)
}
Expand Down
Loading

0 comments on commit 8e8b484

Please sign in to comment.