Skip to content

Commit

Permalink
Merge #2703
Browse files Browse the repository at this point in the history
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 <1939362+davidhewitt@users.noreply.github.com>
  • Loading branch information
bors[bot] and davidhewitt authored Jan 15, 2023
2 parents ed0f338 + 8f48d15 commit 556b3cf
Show file tree
Hide file tree
Showing 11 changed files with 54 additions and 17 deletions.
1 change: 1 addition & 0 deletions newsfragments/2703.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +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.
4 changes: 2 additions & 2 deletions pyo3-macros-backend/src/deprecations.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
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 {
PyFunctionArguments,
PyMethodArgsAttribute,
RequiredArgumentAfterOption,
}

impl Deprecation {
fn ident(&self, span: Span) -> syn::Ident {
let string = match self {
Deprecation::PyFunctionArguments => "PYFUNCTION_ARGUMENTS",
Deprecation::PyMethodArgsAttribute => "PYMETHODS_ARGS_ATTRIBUTE",
Deprecation::RequiredArgumentAfterOption => "REQUIRED_ARGUMENT_AFTER_OPTION",
};
syn::Ident::new(string, span)
}
Expand Down
2 changes: 1 addition & 1 deletion pyo3-macros-backend/src/method.rs
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@ impl<'a> FnSpec<'a> {
} else if let Some(deprecated_args) = deprecated_args {
FunctionSignature::from_arguments_and_deprecated_args(arguments, deprecated_args)?
} else {
FunctionSignature::from_arguments(arguments)
FunctionSignature::from_arguments(arguments, &mut deprecations)
};

let text_signature_string = match &fn_type {
Expand Down
4 changes: 2 additions & 2 deletions pyo3-macros-backend/src/pyfunction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,7 @@ pub fn impl_wrap_pyfunction(
deprecated_args,
signature,
text_signature,
deprecations,
mut deprecations,
krate,
} = options;

Expand Down Expand Up @@ -404,7 +404,7 @@ pub fn impl_wrap_pyfunction(
} else if let Some(deprecated_args) = deprecated_args {
FunctionSignature::from_arguments_and_deprecated_args(arguments, deprecated_args)?
} else {
FunctionSignature::from_arguments(arguments)
FunctionSignature::from_arguments(arguments, &mut deprecations)
};

let ty = method::get_return_info(&func.sig.output);
Expand Down
10 changes: 9 additions & 1 deletion pyo3-macros-backend/src/pyfunction/signature.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use syn::{

use crate::{
attributes::{kw, KeywordAttribute},
deprecations::{Deprecation, Deprecations},
method::{FnArg, FnType},
pyfunction::Argument,
};
Expand Down Expand Up @@ -530,7 +531,7 @@ impl<'a> FunctionSignature<'a> {
}

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

if arg.optional.is_none() {
// This argument is required
if python_signature.required_positional_parameters
!= python_signature.positional_parameters.len()
{
// A previous argument was not required
deprecations.push(Deprecation::RequiredArgumentAfterOption, arg.name.span());
}

python_signature.required_positional_parameters =
python_signature.positional_parameters.len() + 1;
}
Expand Down
1 change: 1 addition & 0 deletions pytests/src/datetime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ fn make_time<'p>(
}

#[pyfunction]
#[pyo3(signature = (hour, minute, second, microsecond, tzinfo, fold))]
fn time_with_fold<'p>(
py: Python<'p>,
hour: u8,
Expand Down
6 changes: 6 additions & 0 deletions src/impl_/deprecations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,9 @@ pub const PYFUNCTION_ARGUMENTS: () = ();
note = "the `#[args]` attribute for `#[methods]` is being replaced by `#[pyo3(signature)]`"
)]
pub const PYMETHODS_ARGS_ATTRIBUTE: () = ();

#[deprecated(
since = "0.18.0",
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"
)]
pub const REQUIRED_ARGUMENT_AFTER_OPTION: () = ();
1 change: 1 addition & 0 deletions tests/test_pyfunction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,7 @@ fn use_pyfunction() {
}

#[test]
#[allow(deprecated)]
fn required_argument_after_option() {
#[pyfunction]
pub fn foo(x: Option<i32>, y: i32) -> i32 {
Expand Down
18 changes: 9 additions & 9 deletions tests/test_text_signature.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,12 +121,12 @@ fn test_function() {
#[test]
fn test_auto_test_signature_function() {
#[pyfunction]
fn my_function(a: i32, b: Option<i32>, c: i32) {
fn my_function(a: i32, b: i32, c: i32) {
let _ = (a, b, c);
}

#[pyfunction(pass_module)]
fn my_function_2(module: &PyModule, a: i32, b: Option<i32>, c: i32) {
fn my_function_2(module: &PyModule, a: i32, b: i32, c: i32) {
let _ = (module, a, b, c);
}

Expand Down Expand Up @@ -173,7 +173,7 @@ fn test_auto_test_signature_method() {

#[pymethods]
impl MyClass {
fn method(&self, a: i32, b: Option<i32>, c: i32) {
fn method(&self, a: i32, b: i32, c: i32) {
let _ = (a, b, c);
}

Expand All @@ -196,12 +196,12 @@ fn test_auto_test_signature_method() {
}

#[staticmethod]
fn staticmethod(a: i32, b: Option<i32>, c: i32) {
fn staticmethod(a: i32, b: i32, c: i32) {
let _ = (a, b, c);
}

#[classmethod]
fn classmethod(cls: &PyType, a: i32, b: Option<i32>, c: i32) {
fn classmethod(cls: &PyType, a: i32, b: i32, c: i32) {
let _ = (cls, a, b, c);
}
}
Expand Down Expand Up @@ -239,7 +239,7 @@ fn test_auto_test_signature_method() {
#[test]
fn test_auto_test_signature_opt_out() {
#[pyfunction(text_signature = None)]
fn my_function(a: i32, b: Option<i32>, c: i32) {
fn my_function(a: i32, b: i32, c: i32) {
let _ = (a, b, c);
}

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

Expand All @@ -265,13 +265,13 @@ fn test_auto_test_signature_opt_out() {

#[staticmethod]
#[pyo3(text_signature = None)]
fn staticmethod(a: i32, b: Option<i32>, c: i32) {
fn staticmethod(a: i32, b: i32, c: i32) {
let _ = (a, b, c);
}

#[classmethod]
#[pyo3(text_signature = None)]
fn classmethod(cls: &PyType, a: i32, b: Option<i32>, c: i32) {
fn classmethod(cls: &PyType, a: i32, b: i32, c: i32) {
let _ = (cls, a, b, c);
}
}
Expand Down
7 changes: 7 additions & 0 deletions tests/ui/deprecations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,22 @@ use pyo3::prelude::*;
#[pyfunction(_opt = "None", x = "5")]
fn function_with_args(_opt: Option<i32>, _x: i32) {}

#[pyfunction]
fn function_with_required_after_option(_opt: Option<i32>, _x: i32) {}

#[pyclass]
struct MyClass;

#[pymethods]
impl MyClass {
#[args(_opt = "None", x = "5")]
fn function_with_args(&self, _opt: Option<i32>, _x: i32) {}

#[args(_has_default = 1)]
fn default_arg_before_required_deprecated(&self, _has_default: isize, _required: isize) {}
}

fn main() {
function_with_required_after_option(None, 0);
function_with_args(None, 0);
}
17 changes: 15 additions & 2 deletions tests/ui/deprecations.stderr
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,21 @@ note: the lint level is defined here
1 | #![deny(deprecated)]
| ^^^^^^^^^^

error: use of deprecated constant `pyo3::impl_::deprecations::REQUIRED_ARGUMENT_AFTER_OPTION`: required arguments after an `Option<_>` argument are ambiguous and being phased out
= help: add a `#[pyo3(signature)]` annotation on this function to unambiguously specify the default values for all optional parameters
--> tests/ui/deprecations.rs:9:59
|
9 | fn function_with_required_after_option(_opt: Option<i32>, _x: i32) {}
| ^^

error: use of deprecated constant `pyo3::impl_::deprecations::PYMETHODS_ARGS_ATTRIBUTE`: the `#[args]` attribute for `#[methods]` is being replaced by `#[pyo3(signature)]`
--> tests/ui/deprecations.rs:16:7
|
16 | #[args(_opt = "None", x = "5")]
| ^^^^

error: use of deprecated constant `pyo3::impl_::deprecations::PYMETHODS_ARGS_ATTRIBUTE`: the `#[args]` attribute for `#[methods]` is being replaced by `#[pyo3(signature)]`
--> tests/ui/deprecations.rs:13:7
--> tests/ui/deprecations.rs:19:7
|
13 | #[args(_opt = "None", x = "5")]
19 | #[args(_has_default = 1)]
| ^^^^

0 comments on commit 556b3cf

Please sign in to comment.