Skip to content

deprecate required argument after Option<T> without signature #2703

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jan 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)]
| ^^^^