Skip to content

Commit 827312d

Browse files
committed
deprecate "trailing optional arguments" implicit default behaviour
1 parent e764067 commit 827312d

15 files changed

+108
-15
lines changed

guide/src/async-await.md

+1
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

+13
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 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`.

pyo3-macros-backend/src/params.rs

+35-3
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,16 @@ pub fn impl_arg_params(
108108
.arguments
109109
.iter()
110110
.enumerate()
111-
.map(|(i, arg)| impl_arg_param(arg, i, &mut 0, holders, ctx))
111+
.map(|(i, arg)| {
112+
impl_arg_param(
113+
arg,
114+
spec.signature.attribute.is_some(),
115+
i,
116+
&mut 0,
117+
holders,
118+
ctx,
119+
)
120+
})
112121
.collect();
113122
return (
114123
quote! {
@@ -148,7 +157,16 @@ pub fn impl_arg_params(
148157
.arguments
149158
.iter()
150159
.enumerate()
151-
.map(|(i, arg)| impl_arg_param(arg, i, &mut option_pos, holders, ctx))
160+
.map(|(i, arg)| {
161+
impl_arg_param(
162+
arg,
163+
spec.signature.attribute.is_some(),
164+
i,
165+
&mut option_pos,
166+
holders,
167+
ctx,
168+
)
169+
})
152170
.collect();
153171

154172
let args_handler = if spec.signature.python_signature.varargs.is_some() {
@@ -211,6 +229,7 @@ pub fn impl_arg_params(
211229

212230
fn impl_arg_param(
213231
arg: &FnArg<'_>,
232+
has_signature_attr: bool,
214233
pos: usize,
215234
option_pos: &mut usize,
216235
holders: &mut Holders,
@@ -224,7 +243,14 @@ fn impl_arg_param(
224243
let from_py_with = format_ident!("from_py_with_{}", pos);
225244
let arg_value = quote!(#args_array[#option_pos].as_deref());
226245
*option_pos += 1;
227-
let tokens = impl_regular_arg_param(arg, from_py_with, arg_value, holders, ctx);
246+
let tokens = impl_regular_arg_param(
247+
arg,
248+
has_signature_attr,
249+
from_py_with,
250+
arg_value,
251+
holders,
252+
ctx,
253+
);
228254
check_arg_for_gil_refs(tokens, holders.push_gil_refs_checker(arg.ty.span()), ctx)
229255
}
230256
FnArg::VarArgs(arg) => {
@@ -259,6 +285,7 @@ fn impl_arg_param(
259285
/// index and the index in option diverge when using py: Python
260286
pub(crate) fn impl_regular_arg_param(
261287
arg: &RegularArg<'_>,
288+
has_signature_attr: bool,
262289
from_py_with: syn::Ident,
263290
arg_value: TokenStream, // expected type: Option<&'a Bound<'py, PyAny>>
264291
holders: &mut Holders,
@@ -315,6 +342,11 @@ pub(crate) fn impl_regular_arg_param(
315342
}
316343
} else if arg.option_wrapped_type.is_some() {
317344
let holder = holders.push_holder(arg.name.span());
345+
let arg_value = if !has_signature_attr {
346+
quote_arg_span! { #pyo3_path::impl_::deprecations::deprecate_implicit_option(#arg_value) }
347+
} else {
348+
quote!(#arg_value)
349+
};
318350
quote_arg_span! {
319351
#pyo3_path::impl_::extract_argument::extract_optional_argument(
320352
#arg_value,

pyo3-macros-backend/src/pymethod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -614,6 +614,7 @@ pub fn impl_py_setter_def(
614614

615615
let tokens = impl_regular_arg_param(
616616
arg,
617+
spec.signature.attribute.is_some(),
617618
ident,
618619
quote!(::std::option::Option::Some(_value.into())),
619620
&mut holders,

pytests/src/datetime.rs

+3
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/impl_/deprecations.rs

+10
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,13 @@ impl<T> std::ops::Deref for OptionGilRefs<T> {
8585
&self.0
8686
}
8787
}
88+
89+
#[deprecated(
90+
since = "0.22.0",
91+
note = "Implicit default for trailing optional arguments is phased out. Add an explicit \
92+
`#[pyo3(signature = (...))]` attribute a to silence this warning. In a future \
93+
pyo3 version `Option<..>` arguments will be treated the same as any other argument."
94+
)]
95+
pub fn deprecate_implicit_option<T>(t: T) -> T {
96+
t
97+
}

src/tests/hygiene/pymethods.rs

+1
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

+1
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

+2
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

+11
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ impl MethSignature {
194194
fn get_optional2(&self, test: Option<i32>) -> Option<i32> {
195195
test
196196
}
197+
#[pyo3(signature=(_t1 = None, t2 = None, _t3 = None))]
197198
fn get_optional_positional(
198199
&self,
199200
_t1: Option<i32>,
@@ -752,11 +753,13 @@ impl MethodWithPyClassArg {
752753
fn inplace_add_pyref(&self, mut other: PyRefMut<'_, MethodWithPyClassArg>) {
753754
other.value += self.value;
754755
}
756+
#[pyo3(signature=(other = None))]
755757
fn optional_add(&self, other: Option<&MethodWithPyClassArg>) -> MethodWithPyClassArg {
756758
MethodWithPyClassArg {
757759
value: self.value + other.map(|o| o.value).unwrap_or(10),
758760
}
759761
}
762+
#[pyo3(signature=(other = None))]
760763
fn optional_inplace_add(&self, other: Option<&mut MethodWithPyClassArg>) {
761764
if let Some(other) = other {
762765
other.value += self.value;
@@ -858,6 +861,7 @@ struct FromSequence {
858861
#[pymethods]
859862
impl FromSequence {
860863
#[new]
864+
#[pyo3(signature=(seq = None))]
861865
fn new(seq: Option<&Bound<'_, PySequence>>) -> PyResult<Self> {
862866
if let Some(seq) = seq {
863867
Ok(FromSequence {
@@ -1033,6 +1037,7 @@ macro_rules! issue_1506 {
10331037
issue_1506!(
10341038
#[pymethods]
10351039
impl Issue1506 {
1040+
#[pyo3(signature = (_arg, _args, _kwargs=None))]
10361041
fn issue_1506(
10371042
&self,
10381043
_py: Python<'_>,
@@ -1042,6 +1047,7 @@ issue_1506!(
10421047
) {
10431048
}
10441049

1050+
#[pyo3(signature = (_arg, _args, _kwargs=None))]
10451051
fn issue_1506_mut(
10461052
&mut self,
10471053
_py: Python<'_>,
@@ -1051,6 +1057,7 @@ issue_1506!(
10511057
) {
10521058
}
10531059

1060+
#[pyo3(signature = (_arg, _args, _kwargs=None))]
10541061
fn issue_1506_custom_receiver(
10551062
_slf: Py<Self>,
10561063
_py: Python<'_>,
@@ -1060,6 +1067,7 @@ issue_1506!(
10601067
) {
10611068
}
10621069

1070+
#[pyo3(signature = (_arg, _args, _kwargs=None))]
10631071
fn issue_1506_custom_receiver_explicit(
10641072
_slf: Py<Issue1506>,
10651073
_py: Python<'_>,
@@ -1070,6 +1078,7 @@ issue_1506!(
10701078
}
10711079

10721080
#[new]
1081+
#[pyo3(signature = (_arg, _args, _kwargs=None))]
10731082
fn issue_1506_new(
10741083
_py: Python<'_>,
10751084
_arg: &Bound<'_, PyAny>,
@@ -1088,6 +1097,7 @@ issue_1506!(
10881097
fn issue_1506_setter(&self, _py: Python<'_>, _value: i32) {}
10891098

10901099
#[staticmethod]
1100+
#[pyo3(signature = (_arg, _args, _kwargs=None))]
10911101
fn issue_1506_static(
10921102
_py: Python<'_>,
10931103
_arg: &Bound<'_, PyAny>,
@@ -1097,6 +1107,7 @@ issue_1506!(
10971107
}
10981108

10991109
#[classmethod]
1110+
#[pyo3(signature = (_arg, _args, _kwargs=None))]
11001111
fn issue_1506_class(
11011112
_cls: &Bound<'_, PyType>,
11021113
_py: Python<'_>,

tests/test_pyfunction.rs

+2
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,7 @@ struct ValueClass {
216216
}
217217

218218
#[pyfunction]
219+
#[pyo3(signature=(str_arg, int_arg, tuple_arg, option_arg = None, struct_arg = None))]
219220
fn conversion_error(
220221
str_arg: &str,
221222
int_arg: i64,
@@ -542,6 +543,7 @@ fn test_some_wrap_arguments() {
542543
#[test]
543544
fn test_reference_to_bound_arguments() {
544545
#[pyfunction]
546+
#[pyo3(signature = (x, y = None))]
545547
fn reference_args<'py>(
546548
x: &Bound<'py, PyAny>,
547549
y: Option<&Bound<'py, PyAny>>,

tests/test_sequence.rs

+1
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

+1
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
}

tests/ui/deprecations.rs

+8
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,16 @@ fn pyfunction_from_py_with(
108108
fn pyfunction_gil_ref(_any: &PyAny) {}
109109

110110
#[pyfunction]
111+
#[pyo3(signature = (_any))]
111112
fn pyfunction_option_gil_ref(_any: Option<&PyAny>) {}
112113

114+
#[pyfunction]
115+
#[pyo3(signature = (_i, _any=None))]
116+
fn pyfunction_option_1(_i: u32, _any: Option<i32>) {}
117+
118+
#[pyfunction]
119+
fn pyfunction_option_2(_i: u32, _any: Option<i32>) {}
120+
113121
#[derive(Debug, FromPyObject)]
114122
pub struct Zap {
115123
#[pyo3(item)]

tests/ui/deprecations.stderr

+18-12
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ note: the lint level is defined here
1010
1 | #![deny(deprecated)]
1111
| ^^^^^^^^^^
1212

13+
error: use of deprecated function `pyo3::deprecations::deprecate_implicit_option`: Implicit default for trailing optional arguments is phased out. Add an explicit `#[pyo3(signature = (...))]` attribute a to silence this warning. In a future pyo3 version `Option<..>` arguments will be treated the same as any other argument.
14+
--> tests/ui/deprecations.rs:119:39
15+
|
16+
119 | fn pyfunction_option_2(_i: u32, _any: Option<i32>) {}
17+
| ^^^^^^
18+
1319
error: use of deprecated struct `pyo3::PyCell`: `PyCell` was merged into `Bound`, use that instead; see the migration guide for more info
1420
--> tests/ui/deprecations.rs:23:30
1521
|
@@ -77,39 +83,39 @@ error: use of deprecated method `pyo3::deprecations::GilRefs::<T>::function_arg`
7783
| ^
7884

7985
error: use of deprecated method `pyo3::deprecations::OptionGilRefs::<std::option::Option<T>>::function_arg`: use `Option<&Bound<'_, T>>` instead for this function argument
80-
--> tests/ui/deprecations.rs:111:36
86+
--> tests/ui/deprecations.rs:112:36
8187
|
82-
111 | fn pyfunction_option_gil_ref(_any: Option<&PyAny>) {}
88+
112 | fn pyfunction_option_gil_ref(_any: Option<&PyAny>) {}
8389
| ^^^^^^
8490

8591
error: use of deprecated method `pyo3::deprecations::GilRefs::<T>::from_py_with_arg`: use `&Bound<'_, PyAny>` as the argument for this `from_py_with` extractor
86-
--> tests/ui/deprecations.rs:118:27
92+
--> tests/ui/deprecations.rs:126:27
8793
|
88-
118 | #[pyo3(from_py_with = "PyAny::len", item("my_object"))]
94+
126 | #[pyo3(from_py_with = "PyAny::len", item("my_object"))]
8995
| ^^^^^^^^^^^^
9096

9197
error: use of deprecated method `pyo3::deprecations::GilRefs::<T>::from_py_with_arg`: use `&Bound<'_, PyAny>` as the argument for this `from_py_with` extractor
92-
--> tests/ui/deprecations.rs:128:27
98+
--> tests/ui/deprecations.rs:136:27
9399
|
94-
128 | #[pyo3(from_py_with = "PyAny::len")] usize,
100+
136 | #[pyo3(from_py_with = "PyAny::len")] usize,
95101
| ^^^^^^^^^^^^
96102

97103
error: use of deprecated method `pyo3::deprecations::GilRefs::<T>::from_py_with_arg`: use `&Bound<'_, PyAny>` as the argument for this `from_py_with` extractor
98-
--> tests/ui/deprecations.rs:134:31
104+
--> tests/ui/deprecations.rs:142:31
99105
|
100-
134 | Zip(#[pyo3(from_py_with = "extract_gil_ref")] i32),
106+
142 | Zip(#[pyo3(from_py_with = "extract_gil_ref")] i32),
101107
| ^^^^^^^^^^^^^^^^^
102108

103109
error: use of deprecated method `pyo3::deprecations::GilRefs::<T>::from_py_with_arg`: use `&Bound<'_, PyAny>` as the argument for this `from_py_with` extractor
104-
--> tests/ui/deprecations.rs:141:27
110+
--> tests/ui/deprecations.rs:149:27
105111
|
106-
141 | #[pyo3(from_py_with = "extract_gil_ref")]
112+
149 | #[pyo3(from_py_with = "extract_gil_ref")]
107113
| ^^^^^^^^^^^^^^^^^
108114

109115
error: use of deprecated method `pyo3::deprecations::GilRefs::<pyo3::Python<'_>>::is_python`: use `wrap_pyfunction_bound!` instead
110-
--> tests/ui/deprecations.rs:154:13
116+
--> tests/ui/deprecations.rs:162:13
111117
|
112-
154 | let _ = wrap_pyfunction!(double, py);
118+
162 | let _ = wrap_pyfunction!(double, py);
113119
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
114120
|
115121
= note: this error originates in the macro `wrap_pyfunction` (in Nightly builds, run with -Z macro-backtrace for more info)

0 commit comments

Comments
 (0)