Skip to content
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

[DNM] Disallow optional arguments after positional arguments #4

Closed

Conversation

juniperparsnips
Copy link

When a rust binding has an optional parameter followed by a required parameter it may cause a hard to follow panic in python.
To reproduce:

use pyo3::prelude::*;

#[pyclass]
struct Foo {
    #[pyo3(get, set)]
    pub a: i32
}

#[pymethods]
impl Foo {
    #[new]
    fn new() -> Self {
        Self { a: 0 }
    }

    fn opt_first(&mut self, a: Option<i32>, b: i32) {
        self.a = a.unwrap_or(b)
    }
}

#[pymodule]
fn pyo3test(_py: Python, m: &PyModule) -> PyResult<()> {
    m.add_class::<Foo>()?;
    Ok(())
}
>>> import pyo3test
>>> foo = pyo3test.Foo()
>>> foo.opt_first()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Foo.opt_first() missing 1 required positional argument: 'a'
>>> foo.opt_first(1)
thread '<unnamed>' panicked at 'Failed to extract required method argument', src/lib.rs:24:48
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
pyo3_runtime.PanicException: Failed to extract required method argument
>>> foo.opt_first(1, 2)
>>> foo.a
1
>>>

While the python type hinting states that there is only one required positional argument, the function behaves as if there are two required arguments, and even if one were optional, it would be 'a', not 'b'. Additionally, this behavior leads to illegal python, as def opt_first(a: Optional[int]=None, b: int) triggers the "non-default argument follows default argument" syntax error.

Reworking the proc-macro so that an Option followed by a required positional parameter must be provided would make the behavior of Options inconsistent, so we should explicitly disallow required parameters that follow optional parameters.

Both of the following now fail at compile time rather than runtime,

#[pymethods]
impl Foo {
    fn opt_first(&mut self, a: Option<i32>, b: i32) {
        self.a = a.unwrap_or(b)
    }

    #[args(a = "1")]
    fn default_first_provided(&mut self, a: i32, b: i32) {
        self.a = a + b;
    }
}

While these still compile and work as expected,

#[pymethods]
impl Foo {
    #[args(b = "5")]
    fn default_last_provided(&mut self, a: Option<i32>, b: i32) {
        self.a = a.unwrap_or(b)
    }

    fn opt_last(&mut self, a:i32, b: Option<i32>) {
        self.a = b.unwrap_or(a)
    }
}

@juniperparsnips juniperparsnips force-pushed the parsons20/optional-parameters-must-be-last branch 3 times, most recently from 5659e4e to b344abb Compare November 30, 2021 15:11
Copy link

@Property404 Property404 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent work

@@ -101,3 +101,15 @@ error: `pass_module` cannot be used on Python methods
|
112 | #[pyo3(pass_module)]
| ^^^^^^^^^^^

error: Required positional parameters can't come after optional parameters

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: looks like the other error messages use "cannot"

@Property404
Copy link

Wait - we can do foo.opt_first(None, 1), right? That's what was done in the example you changed

@juniperparsnips
Copy link
Author

juniperparsnips commented Dec 2, 2021

Wait - we can do foo.opt_first(None, 1), right? That's what was done in the example you changed

Before these changes, yes:

>>> import pyo3test
>>> foo = pyo3test.Foo()
>>> foo.opt_first(None, 1)
>>> foo.a
1

@juniperparsnips juniperparsnips force-pushed the parsons20/optional-parameters-must-be-last branch from b344abb to 0bbc3ad Compare December 7, 2021 14:41
@juniperparsnips
Copy link
Author

  • "can't" => "cannot"

@juniperparsnips juniperparsnips changed the title Disallow optional arguments after positional arguments [DNM] Disallow optional arguments after positional arguments Dec 7, 2021
@juniperparsnips
Copy link
Author

Merge to starry/0.15.1 instead.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants