Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add #[pyo3(signature = (...))] attribute #2702

Merged
merged 3 commits into from
Oct 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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))]
davidhewitt marked this conversation as resolved.
Show resolved Hide resolved
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