Skip to content

Commit 556b3cf

Browse files
Merge #2703
2703: deprecate required argument after `Option<T>` without signature r=davidhewitt a=davidhewitt This PR is a follow-up to #2702 to make required arguments after `Option<T>` arguments require a `#[pyo3(signature)]` annotation to remove the possible ambiguity on whether the developer actually wanted to have a required option (which is what PyO3 is currently forced to assume). Co-authored-by: David Hewitt <[email protected]>
2 parents ed0f338 + 8f48d15 commit 556b3cf

File tree

11 files changed

+54
-17
lines changed

11 files changed

+54
-17
lines changed

newsfragments/2703.changed.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Deprecate required arguments after `Option<T>` arguments to `#[pyfunction]` and `#[pymethods]` without also using `#[pyo3(signature)]` to specify whether the arguments should be required or have defaults.

pyo3-macros-backend/src/deprecations.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
use proc_macro2::{Span, TokenStream};
22
use quote::{quote_spanned, ToTokens};
33

4-
// Clippy complains all these variants have the same prefix "Py"...
5-
#[allow(clippy::enum_variant_names)]
64
pub enum Deprecation {
75
PyFunctionArguments,
86
PyMethodArgsAttribute,
7+
RequiredArgumentAfterOption,
98
}
109

1110
impl Deprecation {
1211
fn ident(&self, span: Span) -> syn::Ident {
1312
let string = match self {
1413
Deprecation::PyFunctionArguments => "PYFUNCTION_ARGUMENTS",
1514
Deprecation::PyMethodArgsAttribute => "PYMETHODS_ARGS_ATTRIBUTE",
15+
Deprecation::RequiredArgumentAfterOption => "REQUIRED_ARGUMENT_AFTER_OPTION",
1616
};
1717
syn::Ident::new(string, span)
1818
}

pyo3-macros-backend/src/method.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,7 @@ impl<'a> FnSpec<'a> {
312312
} else if let Some(deprecated_args) = deprecated_args {
313313
FunctionSignature::from_arguments_and_deprecated_args(arguments, deprecated_args)?
314314
} else {
315-
FunctionSignature::from_arguments(arguments)
315+
FunctionSignature::from_arguments(arguments, &mut deprecations)
316316
};
317317

318318
let text_signature_string = match &fn_type {

pyo3-macros-backend/src/pyfunction.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -366,7 +366,7 @@ pub fn impl_wrap_pyfunction(
366366
deprecated_args,
367367
signature,
368368
text_signature,
369-
deprecations,
369+
mut deprecations,
370370
krate,
371371
} = options;
372372

@@ -404,7 +404,7 @@ pub fn impl_wrap_pyfunction(
404404
} else if let Some(deprecated_args) = deprecated_args {
405405
FunctionSignature::from_arguments_and_deprecated_args(arguments, deprecated_args)?
406406
} else {
407-
FunctionSignature::from_arguments(arguments)
407+
FunctionSignature::from_arguments(arguments, &mut deprecations)
408408
};
409409

410410
let ty = method::get_return_info(&func.sig.output);

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use syn::{
1212

1313
use crate::{
1414
attributes::{kw, KeywordAttribute},
15+
deprecations::{Deprecation, Deprecations},
1516
method::{FnArg, FnType},
1617
pyfunction::Argument,
1718
};
@@ -530,7 +531,7 @@ impl<'a> FunctionSignature<'a> {
530531
}
531532

532533
/// Without `#[pyo3(signature)]` or `#[args]` - just take the Rust function arguments as positional.
533-
pub fn from_arguments(mut arguments: Vec<FnArg<'a>>) -> Self {
534+
pub fn from_arguments(mut arguments: Vec<FnArg<'a>>, deprecations: &mut Deprecations) -> Self {
534535
let mut python_signature = PythonSignature::default();
535536
for arg in &arguments {
536537
// Python<'_> arguments don't show in Python signature
@@ -540,6 +541,13 @@ impl<'a> FunctionSignature<'a> {
540541

541542
if arg.optional.is_none() {
542543
// This argument is required
544+
if python_signature.required_positional_parameters
545+
!= python_signature.positional_parameters.len()
546+
{
547+
// A previous argument was not required
548+
deprecations.push(Deprecation::RequiredArgumentAfterOption, arg.name.span());
549+
}
550+
543551
python_signature.required_positional_parameters =
544552
python_signature.positional_parameters.len() + 1;
545553
}

pytests/src/datetime.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ fn make_time<'p>(
3737
}
3838

3939
#[pyfunction]
40+
#[pyo3(signature = (hour, minute, second, microsecond, tzinfo, fold))]
4041
fn time_with_fold<'p>(
4142
py: Python<'p>,
4243
hour: u8,

src/impl_/deprecations.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,9 @@ pub const PYFUNCTION_ARGUMENTS: () = ();
1111
note = "the `#[args]` attribute for `#[methods]` is being replaced by `#[pyo3(signature)]`"
1212
)]
1313
pub const PYMETHODS_ARGS_ATTRIBUTE: () = ();
14+
15+
#[deprecated(
16+
since = "0.18.0",
17+
note = "required arguments after an `Option<_>` argument are ambiguous and being phased out\n= help: add a `#[pyo3(signature)]` annotation on this function to unambiguously specify the default values for all optional parameters"
18+
)]
19+
pub const REQUIRED_ARGUMENT_AFTER_OPTION: () = ();

tests/test_pyfunction.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,7 @@ fn use_pyfunction() {
477477
}
478478

479479
#[test]
480+
#[allow(deprecated)]
480481
fn required_argument_after_option() {
481482
#[pyfunction]
482483
pub fn foo(x: Option<i32>, y: i32) -> i32 {

tests/test_text_signature.rs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -121,12 +121,12 @@ fn test_function() {
121121
#[test]
122122
fn test_auto_test_signature_function() {
123123
#[pyfunction]
124-
fn my_function(a: i32, b: Option<i32>, c: i32) {
124+
fn my_function(a: i32, b: i32, c: i32) {
125125
let _ = (a, b, c);
126126
}
127127

128128
#[pyfunction(pass_module)]
129-
fn my_function_2(module: &PyModule, a: i32, b: Option<i32>, c: i32) {
129+
fn my_function_2(module: &PyModule, a: i32, b: i32, c: i32) {
130130
let _ = (module, a, b, c);
131131
}
132132

@@ -173,7 +173,7 @@ fn test_auto_test_signature_method() {
173173

174174
#[pymethods]
175175
impl MyClass {
176-
fn method(&self, a: i32, b: Option<i32>, c: i32) {
176+
fn method(&self, a: i32, b: i32, c: i32) {
177177
let _ = (a, b, c);
178178
}
179179

@@ -196,12 +196,12 @@ fn test_auto_test_signature_method() {
196196
}
197197

198198
#[staticmethod]
199-
fn staticmethod(a: i32, b: Option<i32>, c: i32) {
199+
fn staticmethod(a: i32, b: i32, c: i32) {
200200
let _ = (a, b, c);
201201
}
202202

203203
#[classmethod]
204-
fn classmethod(cls: &PyType, a: i32, b: Option<i32>, c: i32) {
204+
fn classmethod(cls: &PyType, a: i32, b: i32, c: i32) {
205205
let _ = (cls, a, b, c);
206206
}
207207
}
@@ -239,7 +239,7 @@ fn test_auto_test_signature_method() {
239239
#[test]
240240
fn test_auto_test_signature_opt_out() {
241241
#[pyfunction(text_signature = None)]
242-
fn my_function(a: i32, b: Option<i32>, c: i32) {
242+
fn my_function(a: i32, b: i32, c: i32) {
243243
let _ = (a, b, c);
244244
}
245245

@@ -254,7 +254,7 @@ fn test_auto_test_signature_opt_out() {
254254
#[pymethods]
255255
impl MyClass {
256256
#[pyo3(text_signature = None)]
257-
fn method(&self, a: i32, b: Option<i32>, c: i32) {
257+
fn method(&self, a: i32, b: i32, c: i32) {
258258
let _ = (a, b, c);
259259
}
260260

@@ -265,13 +265,13 @@ fn test_auto_test_signature_opt_out() {
265265

266266
#[staticmethod]
267267
#[pyo3(text_signature = None)]
268-
fn staticmethod(a: i32, b: Option<i32>, c: i32) {
268+
fn staticmethod(a: i32, b: i32, c: i32) {
269269
let _ = (a, b, c);
270270
}
271271

272272
#[classmethod]
273273
#[pyo3(text_signature = None)]
274-
fn classmethod(cls: &PyType, a: i32, b: Option<i32>, c: i32) {
274+
fn classmethod(cls: &PyType, a: i32, b: i32, c: i32) {
275275
let _ = (cls, a, b, c);
276276
}
277277
}

tests/ui/deprecations.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,22 @@ use pyo3::prelude::*;
55
#[pyfunction(_opt = "None", x = "5")]
66
fn function_with_args(_opt: Option<i32>, _x: i32) {}
77

8+
#[pyfunction]
9+
fn function_with_required_after_option(_opt: Option<i32>, _x: i32) {}
10+
811
#[pyclass]
912
struct MyClass;
1013

1114
#[pymethods]
1215
impl MyClass {
1316
#[args(_opt = "None", x = "5")]
1417
fn function_with_args(&self, _opt: Option<i32>, _x: i32) {}
18+
19+
#[args(_has_default = 1)]
20+
fn default_arg_before_required_deprecated(&self, _has_default: isize, _required: isize) {}
1521
}
1622

1723
fn main() {
24+
function_with_required_after_option(None, 0);
1825
function_with_args(None, 0);
1926
}

0 commit comments

Comments
 (0)