Skip to content

Commit 9c728f5

Browse files
committed
makes trailing optional arguments a hard error (see #2935)
1 parent 0fb3623 commit 9c728f5

File tree

11 files changed

+78
-175
lines changed

11 files changed

+78
-175
lines changed

guide/src/function/signature.md

Lines changed: 1 addition & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
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.
44

5-
Like Python, by default PyO3 accepts all arguments as either positional or keyword arguments. Most arguments are required by default, except for trailing `Option<_>` arguments, which are [implicitly given a default of `None`](#trailing-optional-arguments). This behaviour can be configured by the `#[pyo3(signature = (...))]` option which allows writing a signature in Python syntax.
5+
Like Python, by default PyO3 accepts all arguments as either positional or keyword arguments. All arguments are required by default. This behaviour can be configured by the `#[pyo3(signature = (...))]` option which allows writing a signature in Python syntax.
66

77
This section of the guide goes into detail about use of the `#[pyo3(signature = (...))]` option and its related option `#[pyo3(text_signature = "...")]`
88

@@ -118,82 +118,6 @@ num=-1
118118
> }
119119
> ```
120120
121-
## Trailing optional arguments
122-
123-
<div class="warning">
124-
125-
⚠️ Warning: This behaviour is being phased out 🛠️
126-
127-
The special casing of trailing optional arguments is deprecated. In a future `pyo3` version, arguments of type `Option<..>` will share the same behaviour as other arguments, they are required unless a default is set using `#[pyo3(signature = (...))]`.
128-
129-
This is done to better align the Python and Rust definition of such functions and make it more intuitive to rewrite them from Python in Rust. Specifically `def some_fn(a: int, b: Optional[int]): ...` will not automatically default `b` to `none`, but requires an explicit default if desired, where as in current `pyo3` it is handled the other way around.
130-
131-
During the migration window a `#[pyo3(signature = (...))]` will be required to silence the deprecation warning. After support for trailing optional arguments is fully removed, the signature attribute can be removed if all arguments should be required.
132-
</div>
133-
134-
135-
As a convenience, functions without a `#[pyo3(signature = (...))]` option will treat trailing `Option<T>` arguments as having a default of `None`. In the example below, PyO3 will create `increment` with a signature of `increment(x, amount=None)`.
136-
137-
```rust
138-
#![allow(deprecated)]
139-
use pyo3::prelude::*;
140-
141-
/// Returns a copy of `x` increased by `amount`.
142-
///
143-
/// If `amount` is unspecified or `None`, equivalent to `x + 1`.
144-
#[pyfunction]
145-
fn increment(x: u64, amount: Option<u64>) -> u64 {
146-
x + amount.unwrap_or(1)
147-
}
148-
#
149-
# fn main() -> PyResult<()> {
150-
# Python::with_gil(|py| {
151-
# let fun = pyo3::wrap_pyfunction!(increment, py)?;
152-
#
153-
# let inspect = PyModule::import(py, "inspect")?.getattr("signature")?;
154-
# let sig: String = inspect
155-
# .call1((fun,))?
156-
# .call_method0("__str__")?
157-
# .extract()?;
158-
#
159-
# #[cfg(Py_3_8)] // on 3.7 the signature doesn't render b, upstream bug?
160-
# assert_eq!(sig, "(x, amount=None)");
161-
#
162-
# Ok(())
163-
# })
164-
# }
165-
```
166-
167-
To make trailing `Option<T>` arguments required, but still accept `None`, add a `#[pyo3(signature = (...))]` annotation. For the example above, this would be `#[pyo3(signature = (x, amount))]`:
168-
169-
```rust
170-
# use pyo3::prelude::*;
171-
#[pyfunction]
172-
#[pyo3(signature = (x, amount))]
173-
fn increment(x: u64, amount: Option<u64>) -> u64 {
174-
x + amount.unwrap_or(1)
175-
}
176-
#
177-
# fn main() -> PyResult<()> {
178-
# Python::with_gil(|py| {
179-
# let fun = pyo3::wrap_pyfunction!(increment, py)?;
180-
#
181-
# let inspect = PyModule::import(py, "inspect")?.getattr("signature")?;
182-
# let sig: String = inspect
183-
# .call1((fun,))?
184-
# .call_method0("__str__")?
185-
# .extract()?;
186-
#
187-
# #[cfg(Py_3_8)] // on 3.7 the signature doesn't render b, upstream bug?
188-
# assert_eq!(sig, "(x, amount)");
189-
#
190-
# Ok(())
191-
# })
192-
# }
193-
```
194-
195-
To help avoid confusion, PyO3 requires `#[pyo3(signature = (...))]` when an `Option<T>` argument is surrounded by arguments which aren't `Option<T>`.
196-
197121
## Making the function signature available to Python
198122
199123
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.

guide/src/migration.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,7 @@ and unnoticed changes in behavior. With 0.24 this restriction will be lifted aga
282282

283283
Before:
284284

285-
```rust
285+
```rust,ignore
286286
# #![allow(deprecated, dead_code)]
287287
# use pyo3::prelude::*;
288288
#[pyfunction]
@@ -872,7 +872,7 @@ Python::with_gil(|py| -> PyResult<()> {
872872
<details>
873873
<summary><small>Click to expand</small></summary>
874874

875-
[Trailing `Option<T>` arguments](./function/signature.md#trailing-optional-arguments) have an automatic default of `None`. To avoid unwanted changes when modifying function signatures, in PyO3 0.18 it was deprecated to have a required argument after an `Option<T>` argument without using `#[pyo3(signature = (...))]` to specify the intended defaults. In PyO3 0.20, this becomes a hard error.
875+
Trailing `Option<T>` arguments have an automatic default of `None`. To avoid unwanted changes when modifying function signatures, in PyO3 0.18 it was deprecated to have a required argument after an `Option<T>` argument without using `#[pyo3(signature = (...))]` to specify the intended defaults. In PyO3 0.20, this becomes a hard error.
876876

877877
Before:
878878

@@ -1095,7 +1095,7 @@ Starting with PyO3 0.18, this is deprecated and a future PyO3 version will requi
10951095

10961096
Before, x in the below example would be required to be passed from Python code:
10971097

1098-
```rust,compile_fail
1098+
```rust,compile_fail,ignore
10991099
# #![allow(dead_code)]
11001100
# use pyo3::prelude::*;
11011101

newsfragments/4729.changed.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
makes trailing optional arguments a hard error (see #2935)
Lines changed: 17 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,24 @@
1-
use crate::method::{FnArg, FnSpec};
2-
use proc_macro2::TokenStream;
3-
use quote::quote_spanned;
1+
use crate::method::{FnArg, FnSpec, RegularArg};
42

5-
pub(crate) fn deprecate_trailing_option_default(spec: &FnSpec<'_>) -> TokenStream {
3+
pub(crate) fn deprecate_trailing_option_default(spec: &FnSpec<'_>) -> syn::Result<()> {
64
if spec.signature.attribute.is_none()
75
&& spec.tp.signature_attribute_allowed()
8-
&& spec.signature.arguments.iter().any(|arg| {
9-
if let FnArg::Regular(arg) = arg {
10-
arg.option_wrapped_type.is_some()
11-
} else {
12-
false
13-
}
6+
&& spec.signature.arguments.iter().last().map_or(false, |arg| {
7+
matches!(
8+
arg,
9+
FnArg::Regular(RegularArg {
10+
option_wrapped_type: Some(..),
11+
..
12+
}),
13+
)
1414
})
1515
{
1616
use std::fmt::Write;
1717
let mut deprecation_msg = String::from(
18-
"this function has implicit defaults for the trailing `Option<T>` arguments \n\
19-
= note: these implicit defaults are being phased out \n\
18+
"this function uses trailing `Option<T>` arguments \n\
19+
= note: these used to be implicitly default to `None` \n\
20+
= note: this behaviour is phased out \n\
21+
= note: in the future this error will be lifted and trailing `Option`s be treated as any other argument \n\
2022
= help: add `#[pyo3(signature = (",
2123
);
2224
spec.signature.arguments.iter().for_each(|arg| {
@@ -39,16 +41,9 @@ pub(crate) fn deprecate_trailing_option_default(spec: &FnSpec<'_>) -> TokenStrea
3941
deprecation_msg.pop();
4042
deprecation_msg.pop();
4143

42-
deprecation_msg.push_str(
43-
"))]` to this function to silence this warning and keep the current behavior",
44-
);
45-
quote_spanned! { spec.name.span() =>
46-
#[deprecated(note = #deprecation_msg)]
47-
#[allow(dead_code)]
48-
const SIGNATURE: () = ();
49-
const _: () = SIGNATURE;
50-
}
44+
deprecation_msg.push_str("))]` to this function to keep the previous behavior");
45+
Err(syn::Error::new(spec.name.span(), deprecation_msg))
5146
} else {
52-
TokenStream::new()
47+
Ok(())
5348
}
5449
}

pyo3-macros-backend/src/method.rs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -745,7 +745,7 @@ impl<'a> FnSpec<'a> {
745745
quote!(#func_name)
746746
};
747747

748-
let deprecation = deprecate_trailing_option_default(self);
748+
deprecate_trailing_option_default(self)?;
749749

750750
Ok(match self.convention {
751751
CallingConvention::Noargs => {
@@ -767,7 +767,6 @@ impl<'a> FnSpec<'a> {
767767
py: #pyo3_path::Python<'py>,
768768
_slf: *mut #pyo3_path::ffi::PyObject,
769769
) -> #pyo3_path::PyResult<*mut #pyo3_path::ffi::PyObject> {
770-
#deprecation
771770
let function = #rust_name; // Shadow the function name to avoid #3017
772771
#init_holders
773772
let result = #call;
@@ -789,7 +788,6 @@ impl<'a> FnSpec<'a> {
789788
_nargs: #pyo3_path::ffi::Py_ssize_t,
790789
_kwnames: *mut #pyo3_path::ffi::PyObject
791790
) -> #pyo3_path::PyResult<*mut #pyo3_path::ffi::PyObject> {
792-
#deprecation
793791
let function = #rust_name; // Shadow the function name to avoid #3017
794792
#arg_convert
795793
#init_holders
@@ -811,7 +809,6 @@ impl<'a> FnSpec<'a> {
811809
_args: *mut #pyo3_path::ffi::PyObject,
812810
_kwargs: *mut #pyo3_path::ffi::PyObject
813811
) -> #pyo3_path::PyResult<*mut #pyo3_path::ffi::PyObject> {
814-
#deprecation
815812
let function = #rust_name; // Shadow the function name to avoid #3017
816813
#arg_convert
817814
#init_holders
@@ -836,7 +833,6 @@ impl<'a> FnSpec<'a> {
836833
_kwargs: *mut #pyo3_path::ffi::PyObject
837834
) -> #pyo3_path::PyResult<*mut #pyo3_path::ffi::PyObject> {
838835
use #pyo3_path::impl_::callback::IntoPyCallbackOutput;
839-
#deprecation
840836
let function = #rust_name; // Shadow the function name to avoid #3017
841837
#arg_convert
842838
#init_holders

pyo3-macros-backend/src/params.rs

Lines changed: 25 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -245,10 +245,7 @@ pub(crate) fn impl_regular_arg_param(
245245
// Option<T> arguments have special treatment: the default should be specified _without_ the
246246
// Some() wrapper. Maybe this should be changed in future?!
247247
if arg.option_wrapped_type.is_some() {
248-
default = Some(default.map_or_else(
249-
|| quote!(::std::option::Option::None),
250-
|tokens| some_wrap(tokens, ctx),
251-
));
248+
default = default.map(|tokens| some_wrap(tokens, ctx));
252249
}
253250

254251
if arg.from_py_with.is_some() {
@@ -273,31 +270,32 @@ pub(crate) fn impl_regular_arg_param(
273270
)?
274271
}
275272
}
276-
} else if arg.option_wrapped_type.is_some() {
277-
let holder = holders.push_holder(arg.name.span());
278-
quote_arg_span! {
279-
#pyo3_path::impl_::extract_argument::extract_optional_argument(
280-
#arg_value,
281-
&mut #holder,
282-
#name_str,
283-
#[allow(clippy::redundant_closure)]
284-
{
285-
|| #default
286-
}
287-
)?
288-
}
289273
} else if let Some(default) = default {
290274
let holder = holders.push_holder(arg.name.span());
291-
quote_arg_span! {
292-
#pyo3_path::impl_::extract_argument::extract_argument_with_default(
293-
#arg_value,
294-
&mut #holder,
295-
#name_str,
296-
#[allow(clippy::redundant_closure)]
297-
{
298-
|| #default
299-
}
300-
)?
275+
if arg.option_wrapped_type.is_some() {
276+
quote_arg_span! {
277+
#pyo3_path::impl_::extract_argument::extract_optional_argument(
278+
#arg_value,
279+
&mut #holder,
280+
#name_str,
281+
#[allow(clippy::redundant_closure)]
282+
{
283+
|| #default
284+
}
285+
)?
286+
}
287+
} else {
288+
quote_arg_span! {
289+
#pyo3_path::impl_::extract_argument::extract_argument_with_default(
290+
#arg_value,
291+
&mut #holder,
292+
#name_str,
293+
#[allow(clippy::redundant_closure)]
294+
{
295+
|| #default
296+
}
297+
)?
298+
}
301299
}
302300
} else {
303301
let holder = holders.push_holder(arg.name.span());

pyo3-macros-backend/src/pyfunction/signature.rs

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -467,12 +467,7 @@ impl<'a> FunctionSignature<'a> {
467467
continue;
468468
}
469469

470-
if let FnArg::Regular(RegularArg {
471-
ty,
472-
option_wrapped_type: None,
473-
..
474-
}) = arg
475-
{
470+
if let FnArg::Regular(RegularArg { ty, .. }) = arg {
476471
// This argument is required, all previous arguments must also have been required
477472
ensure_spanned!(
478473
python_signature.required_positional_parameters == python_signature.positional_parameters.len(),

pyo3-macros-backend/src/pymethod.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -685,9 +685,8 @@ pub fn impl_py_setter_def(
685685
ctx,
686686
);
687687

688-
let deprecation = deprecate_trailing_option_default(spec);
688+
deprecate_trailing_option_default(spec)?;
689689
quote! {
690-
#deprecation
691690
#from_py_with
692691
let _val = #extract;
693692
}

tests/ui/deprecations.stderr

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,28 @@
1-
error: use of deprecated constant `__pyfunction_pyfunction_option_2::SIGNATURE`: this function has implicit defaults for the trailing `Option<T>` arguments
2-
= note: these implicit defaults are being phased out
3-
= help: add `#[pyo3(signature = (_i, _any=None))]` to this function to silence this warning and keep the current behavior
1+
error: this function uses trailing `Option<T>` arguments
2+
= note: these used to be implicitly default to `None`
3+
= note: this behaviour is phased out
4+
= note: in the future this error will be lifted and trailing `Option`s be treated as any other argument
5+
= help: add `#[pyo3(signature = (_i, _any=None))]` to this function to keep the previous behavior
46
--> tests/ui/deprecations.rs:15:4
57
|
68
15 | fn pyfunction_option_2(_i: u32, _any: Option<i32>) {}
79
| ^^^^^^^^^^^^^^^^^^^
8-
|
9-
note: the lint level is defined here
10-
--> tests/ui/deprecations.rs:1:9
11-
|
12-
1 | #![deny(deprecated)]
13-
| ^^^^^^^^^^
1410

15-
error: use of deprecated constant `__pyfunction_pyfunction_option_3::SIGNATURE`: this function has implicit defaults for the trailing `Option<T>` arguments
16-
= note: these implicit defaults are being phased out
17-
= help: add `#[pyo3(signature = (_i, _any=None, _foo=None))]` to this function to silence this warning and keep the current behavior
11+
error: this function uses trailing `Option<T>` arguments
12+
= note: these used to be implicitly default to `None`
13+
= note: this behaviour is phased out
14+
= note: in the future this error will be lifted and trailing `Option`s be treated as any other argument
15+
= help: add `#[pyo3(signature = (_i, _any=None, _foo=None))]` to this function to keep the previous behavior
1816
--> tests/ui/deprecations.rs:18:4
1917
|
2018
18 | fn pyfunction_option_3(_i: u32, _any: Option<i32>, _foo: Option<String>) {}
2119
| ^^^^^^^^^^^^^^^^^^^
2220

23-
error: use of deprecated constant `__pyfunction_pyfunction_option_4::SIGNATURE`: this function has implicit defaults for the trailing `Option<T>` arguments
24-
= note: these implicit defaults are being phased out
25-
= help: add `#[pyo3(signature = (_i, _any=None, _foo=None))]` to this function to silence this warning and keep the current behavior
21+
error: this function uses trailing `Option<T>` arguments
22+
= note: these used to be implicitly default to `None`
23+
= note: this behaviour is phased out
24+
= note: in the future this error will be lifted and trailing `Option`s be treated as any other argument
25+
= help: add `#[pyo3(signature = (_i, _any=None, _foo=None))]` to this function to keep the previous behavior
2626
--> tests/ui/deprecations.rs:21:4
2727
|
2828
21 | fn pyfunction_option_4(
@@ -34,4 +34,9 @@ error: use of deprecated method `pyo3::impl_::pyclass::Deprecation::autogenerate
3434
28 | #[pyclass]
3535
| ^^^^^^^^^^
3636
|
37+
note: the lint level is defined here
38+
--> tests/ui/deprecations.rs:1:9
39+
|
40+
1 | #![deny(deprecated)]
41+
| ^^^^^^^^^^
3742
= note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info)

tests/ui/invalid_pyfunctions.rs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,6 @@ fn wildcard_argument(_: i32) {}
1313
#[pyfunction]
1414
fn destructured_argument((_a, _b): (i32, i32)) {}
1515

16-
#[pyfunction]
17-
fn function_with_required_after_option(_opt: Option<i32>, _x: i32) {}
18-
1916
#[pyfunction]
2017
#[pyo3(signature=(*args))]
2118
fn function_with_optional_args(args: Option<Bound<'_, PyTuple>>) {

0 commit comments

Comments
 (0)