Skip to content

Commit aef0a05

Browse files
authored
deprecate implicit default for trailing optional arguments (#4078)
* deprecate "trailing optional arguments" implicit default behaviour * add newsfragment * generate individual deprecation messages per function * add migration guide entry
1 parent 104328c commit aef0a05

17 files changed

+210
-25
lines changed

guide/src/async-await.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use futures::channel::oneshot;
1212
use pyo3::prelude::*;
1313

1414
#[pyfunction]
15+
#[pyo3(signature=(seconds, result=None))]
1516
async fn sleep(seconds: f64, result: Option<PyObject>) -> Option<PyObject> {
1617
let (tx, rx) = oneshot::channel();
1718
thread::spawn(move || {

guide/src/function/signature.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,9 +121,22 @@ num=-1
121121
122122
## Trailing optional arguments
123123
124+
<div class="warning">
125+
126+
⚠️ Warning: This behaviour is being phased out 🛠️
127+
128+
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 = (...))]`.
129+
130+
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.
131+
132+
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.
133+
</div>
134+
135+
124136
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)`.
125137
126138
```rust
139+
#![allow(deprecated)]
127140
use pyo3::prelude::*;
128141
129142
/// Returns a copy of `x` increased by `amount`.

guide/src/migration.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,41 @@
33
This guide can help you upgrade code through breaking changes from one PyO3 version to the next.
44
For a detailed list of all changes, see the [CHANGELOG](changelog.md).
55

6+
## from 0.21.* to 0.22
7+
8+
### Deprecation of implicit default for trailing optional arguments
9+
<details open>
10+
<summary><small>Click to expand</small></summary>
11+
12+
With `pyo3` 0.22 the implicit `None` default for trailing `Option<T>` type argument is deprecated. To migrate, place a `#[pyo3(signature = (...))]` attribute on affected functions or methods and specify the desired behavior.
13+
The migration warning specifies the corresponding signature to keep the current behavior. With 0.23 the signature will be required for any function containing `Option<T>` type parameters to prevent accidental
14+
and unnoticed changes in behavior. With 0.24 this restriction will be lifted again and `Option<T>` type arguments will be treated as any other argument _without_ special handling.
15+
16+
Before:
17+
18+
```rust
19+
# #![allow(deprecated, dead_code)]
20+
# use pyo3::prelude::*;
21+
#[pyfunction]
22+
fn increment(x: u64, amount: Option<u64>) -> u64 {
23+
x + amount.unwrap_or(1)
24+
}
25+
```
26+
27+
After:
28+
29+
```rust
30+
# #![allow(dead_code)]
31+
# use pyo3::prelude::*;
32+
#[pyfunction]
33+
#[pyo3(signature = (x, amount=None))]
34+
fn increment(x: u64, amount: Option<u64>) -> u64 {
35+
x + amount.unwrap_or(1)
36+
}
37+
```
38+
39+
</details>
40+
641
## from 0.20.* to 0.21
742
<details open>
843
<summary><small>Click to expand</small></summary>

newsfragments/4078.changed.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
deprecate implicit default for trailing optional arguments

pyo3-macros-backend/src/deprecations.rs

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
use crate::utils::Ctx;
1+
use crate::{
2+
method::{FnArg, FnSpec},
3+
utils::Ctx,
4+
};
25
use proc_macro2::{Span, TokenStream};
36
use quote::{quote_spanned, ToTokens};
47

@@ -45,3 +48,51 @@ impl<'ctx> ToTokens for Deprecations<'ctx> {
4548
}
4649
}
4750
}
51+
52+
pub(crate) fn deprecate_trailing_option_default(spec: &FnSpec<'_>) -> TokenStream {
53+
if spec.signature.attribute.is_none()
54+
&& spec.signature.arguments.iter().any(|arg| {
55+
if let FnArg::Regular(arg) = arg {
56+
arg.option_wrapped_type.is_some()
57+
} else {
58+
false
59+
}
60+
})
61+
{
62+
use std::fmt::Write;
63+
let mut deprecation_msg = String::from(
64+
"This function has implicit defaults for the trailing `Option<T>` arguments. \
65+
These implicit defaults are being phased out. Add `#[pyo3(signature = (",
66+
);
67+
spec.signature.arguments.iter().for_each(|arg| {
68+
match arg {
69+
FnArg::Regular(arg) => {
70+
if arg.option_wrapped_type.is_some() {
71+
write!(deprecation_msg, "{}=None, ", arg.name)
72+
} else {
73+
write!(deprecation_msg, "{}, ", arg.name)
74+
}
75+
}
76+
FnArg::VarArgs(arg) => write!(deprecation_msg, "{}, ", arg.name),
77+
FnArg::KwArgs(arg) => write!(deprecation_msg, "{}, ", arg.name),
78+
FnArg::Py(_) | FnArg::CancelHandle(_) => Ok(()),
79+
}
80+
.expect("writing to `String` should not fail");
81+
});
82+
83+
//remove trailing space and comma
84+
deprecation_msg.pop();
85+
deprecation_msg.pop();
86+
87+
deprecation_msg
88+
.push_str(")]` to this function to silence this warning and keep the current behavior");
89+
quote_spanned! { spec.name.span() =>
90+
#[deprecated(note = #deprecation_msg)]
91+
#[allow(dead_code)]
92+
const SIGNATURE: () = ();
93+
const _: () = SIGNATURE;
94+
}
95+
} else {
96+
TokenStream::new()
97+
}
98+
}

pyo3-macros-backend/src/method.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use proc_macro2::{Span, TokenStream};
55
use quote::{format_ident, quote, quote_spanned, ToTokens};
66
use syn::{ext::IdentExt, spanned::Spanned, Ident, Result};
77

8+
use crate::deprecations::deprecate_trailing_option_default;
89
use crate::utils::Ctx;
910
use crate::{
1011
attributes::{FromPyWithAttribute, TextSignatureAttribute, TextSignatureAttributeValue},
@@ -708,6 +709,8 @@ impl<'a> FnSpec<'a> {
708709
quote!(#func_name)
709710
};
710711

712+
let deprecation = deprecate_trailing_option_default(self);
713+
711714
Ok(match self.convention {
712715
CallingConvention::Noargs => {
713716
let mut holders = Holders::new();
@@ -730,6 +733,7 @@ impl<'a> FnSpec<'a> {
730733
py: #pyo3_path::Python<'py>,
731734
_slf: *mut #pyo3_path::ffi::PyObject,
732735
) -> #pyo3_path::PyResult<*mut #pyo3_path::ffi::PyObject> {
736+
#deprecation
733737
let _slf_ref = &_slf;
734738
let function = #rust_name; // Shadow the function name to avoid #3017
735739
#init_holders
@@ -754,6 +758,7 @@ impl<'a> FnSpec<'a> {
754758
_nargs: #pyo3_path::ffi::Py_ssize_t,
755759
_kwnames: *mut #pyo3_path::ffi::PyObject
756760
) -> #pyo3_path::PyResult<*mut #pyo3_path::ffi::PyObject> {
761+
#deprecation
757762
let _slf_ref = &_slf;
758763
let function = #rust_name; // Shadow the function name to avoid #3017
759764
#arg_convert
@@ -778,6 +783,7 @@ impl<'a> FnSpec<'a> {
778783
_args: *mut #pyo3_path::ffi::PyObject,
779784
_kwargs: *mut #pyo3_path::ffi::PyObject
780785
) -> #pyo3_path::PyResult<*mut #pyo3_path::ffi::PyObject> {
786+
#deprecation
781787
let _slf_ref = &_slf;
782788
let function = #rust_name; // Shadow the function name to avoid #3017
783789
#arg_convert
@@ -805,6 +811,7 @@ impl<'a> FnSpec<'a> {
805811
_kwargs: *mut #pyo3_path::ffi::PyObject
806812
) -> #pyo3_path::PyResult<*mut #pyo3_path::ffi::PyObject> {
807813
use #pyo3_path::callback::IntoPyCallbackOutput;
814+
#deprecation
808815
let _slf_ref = &_slf;
809816
let function = #rust_name; // Shadow the function name to avoid #3017
810817
#arg_convert

pyo3-macros-backend/src/pymethod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use std::borrow::Cow;
22

33
use crate::attributes::{NameAttribute, RenamingRule};
4+
use crate::deprecations::deprecate_trailing_option_default;
45
use crate::method::{CallingConvention, ExtractErrorMode, PyArg};
56
use crate::params::{check_arg_for_gil_refs, impl_regular_arg_param, Holders};
67
use crate::utils::Ctx;
@@ -637,7 +638,10 @@ pub fn impl_py_setter_def(
637638
);
638639
let extract =
639640
check_arg_for_gil_refs(tokens, holders.push_gil_refs_checker(arg.ty.span()), ctx);
641+
642+
let deprecation = deprecate_trailing_option_default(spec);
640643
quote! {
644+
#deprecation
641645
#from_py_with
642646
let _val = #extract;
643647
}

pytests/src/datetime.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ fn date_from_timestamp(py: Python<'_>, timestamp: i64) -> PyResult<Bound<'_, PyD
2525
}
2626

2727
#[pyfunction]
28+
#[pyo3(signature=(hour, minute, second, microsecond, tzinfo=None))]
2829
fn make_time<'py>(
2930
py: Python<'py>,
3031
hour: u8,
@@ -101,6 +102,7 @@ fn get_delta_tuple<'py>(delta: &Bound<'py, PyDelta>) -> Bound<'py, PyTuple> {
101102

102103
#[allow(clippy::too_many_arguments)]
103104
#[pyfunction]
105+
#[pyo3(signature=(year, month, day, hour, minute, second, microsecond, tzinfo=None))]
104106
fn make_datetime<'py>(
105107
py: Python<'py>,
106108
year: i32,
@@ -159,6 +161,7 @@ fn get_datetime_tuple_fold<'py>(dt: &Bound<'py, PyDateTime>) -> Bound<'py, PyTup
159161
}
160162

161163
#[pyfunction]
164+
#[pyo3(signature=(ts, tz=None))]
162165
fn datetime_from_timestamp<'py>(
163166
py: Python<'py>,
164167
ts: f64,

src/tests/hygiene/pymethods.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,7 @@ impl Dummy {
309309
0
310310
}
311311

312+
#[pyo3(signature=(ndigits=::std::option::Option::None))]
312313
fn __round__(&self, ndigits: ::std::option::Option<u32>) -> u32 {
313314
0
314315
}

tests/test_arithmetics.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ impl UnaryArithmetic {
3535
Self::new(self.inner.abs())
3636
}
3737

38+
#[pyo3(signature=(_ndigits=None))]
3839
fn __round__(&self, _ndigits: Option<u32>) -> Self {
3940
Self::new(self.inner.round())
4041
}

tests/test_mapping.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ struct Mapping {
2121
#[pymethods]
2222
impl Mapping {
2323
#[new]
24+
#[pyo3(signature=(elements=None))]
2425
fn new(elements: Option<&Bound<'_, PyList>>) -> PyResult<Self> {
2526
if let Some(pylist) = elements {
2627
let mut elems = HashMap::with_capacity(pylist.len());
@@ -59,6 +60,7 @@ impl Mapping {
5960
}
6061
}
6162

63+
#[pyo3(signature=(key, default=None))]
6264
fn get(&self, py: Python<'_>, key: &str, default: Option<PyObject>) -> Option<PyObject> {
6365
self.index
6466
.get(key)

tests/test_methods.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ impl MethSignature {
187187
fn get_optional2(&self, test: Option<i32>) -> Option<i32> {
188188
test
189189
}
190+
#[pyo3(signature=(_t1 = None, t2 = None, _t3 = None))]
190191
fn get_optional_positional(
191192
&self,
192193
_t1: Option<i32>,
@@ -745,11 +746,13 @@ impl MethodWithPyClassArg {
745746
fn inplace_add_pyref(&self, mut other: PyRefMut<'_, MethodWithPyClassArg>) {
746747
other.value += self.value;
747748
}
749+
#[pyo3(signature=(other = None))]
748750
fn optional_add(&self, other: Option<&MethodWithPyClassArg>) -> MethodWithPyClassArg {
749751
MethodWithPyClassArg {
750752
value: self.value + other.map(|o| o.value).unwrap_or(10),
751753
}
752754
}
755+
#[pyo3(signature=(other = None))]
753756
fn optional_inplace_add(&self, other: Option<&mut MethodWithPyClassArg>) {
754757
if let Some(other) = other {
755758
other.value += self.value;
@@ -851,6 +854,7 @@ struct FromSequence {
851854
#[pymethods]
852855
impl FromSequence {
853856
#[new]
857+
#[pyo3(signature=(seq = None))]
854858
fn new(seq: Option<&Bound<'_, PySequence>>) -> PyResult<Self> {
855859
if let Some(seq) = seq {
856860
Ok(FromSequence {
@@ -1026,6 +1030,7 @@ macro_rules! issue_1506 {
10261030
issue_1506!(
10271031
#[pymethods]
10281032
impl Issue1506 {
1033+
#[pyo3(signature = (_arg, _args, _kwargs=None))]
10291034
fn issue_1506(
10301035
&self,
10311036
_py: Python<'_>,
@@ -1035,6 +1040,7 @@ issue_1506!(
10351040
) {
10361041
}
10371042

1043+
#[pyo3(signature = (_arg, _args, _kwargs=None))]
10381044
fn issue_1506_mut(
10391045
&mut self,
10401046
_py: Python<'_>,
@@ -1044,6 +1050,7 @@ issue_1506!(
10441050
) {
10451051
}
10461052

1053+
#[pyo3(signature = (_arg, _args, _kwargs=None))]
10471054
fn issue_1506_custom_receiver(
10481055
_slf: Py<Self>,
10491056
_py: Python<'_>,
@@ -1053,6 +1060,7 @@ issue_1506!(
10531060
) {
10541061
}
10551062

1063+
#[pyo3(signature = (_arg, _args, _kwargs=None))]
10561064
fn issue_1506_custom_receiver_explicit(
10571065
_slf: Py<Issue1506>,
10581066
_py: Python<'_>,
@@ -1063,6 +1071,7 @@ issue_1506!(
10631071
}
10641072

10651073
#[new]
1074+
#[pyo3(signature = (_arg, _args, _kwargs=None))]
10661075
fn issue_1506_new(
10671076
_py: Python<'_>,
10681077
_arg: &Bound<'_, PyAny>,
@@ -1081,6 +1090,7 @@ issue_1506!(
10811090
fn issue_1506_setter(&self, _py: Python<'_>, _value: i32) {}
10821091

10831092
#[staticmethod]
1093+
#[pyo3(signature = (_arg, _args, _kwargs=None))]
10841094
fn issue_1506_static(
10851095
_py: Python<'_>,
10861096
_arg: &Bound<'_, PyAny>,
@@ -1090,6 +1100,7 @@ issue_1506!(
10901100
}
10911101

10921102
#[classmethod]
1103+
#[pyo3(signature = (_arg, _args, _kwargs=None))]
10931104
fn issue_1506_class(
10941105
_cls: &Bound<'_, PyType>,
10951106
_py: Python<'_>,

tests/test_pyfunction.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ fn test_from_py_with_defaults() {
182182

183183
// issue 2280 combination of from_py_with and Option<T> did not compile
184184
#[pyfunction]
185+
#[pyo3(signature = (int=None))]
185186
fn from_py_with_option(#[pyo3(from_py_with = "optional_int")] int: Option<i32>) -> i32 {
186187
int.unwrap_or(0)
187188
}
@@ -216,6 +217,7 @@ struct ValueClass {
216217
}
217218

218219
#[pyfunction]
220+
#[pyo3(signature=(str_arg, int_arg, tuple_arg, option_arg = None, struct_arg = None))]
219221
fn conversion_error(
220222
str_arg: &str,
221223
int_arg: i64,
@@ -542,6 +544,7 @@ fn test_some_wrap_arguments() {
542544
#[test]
543545
fn test_reference_to_bound_arguments() {
544546
#[pyfunction]
547+
#[pyo3(signature = (x, y = None))]
545548
fn reference_args<'py>(
546549
x: &Bound<'py, PyAny>,
547550
y: Option<&Bound<'py, PyAny>>,

tests/test_sequence.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ struct ByteSequence {
1717
#[pymethods]
1818
impl ByteSequence {
1919
#[new]
20+
#[pyo3(signature=(elements = None))]
2021
fn new(elements: Option<&Bound<'_, PyList>>) -> PyResult<Self> {
2122
if let Some(pylist) = elements {
2223
let mut elems = Vec::with_capacity(pylist.len());

tests/test_text_signature.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ fn test_auto_test_signature_function() {
142142
}
143143

144144
#[pyfunction]
145+
#[pyo3(signature=(a, b=None, c=None))]
145146
fn my_function_6(a: i32, b: Option<i32>, c: Option<i32>) {
146147
let _ = (a, b, c);
147148
}

0 commit comments

Comments
 (0)