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

Positional-only args #1925

Merged
merged 10 commits into from
Oct 19, 2021
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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Add `as_sequence` methods to `PyList` and `PyTuple`. [#1860](https://github.com/PyO3/pyo3/pull/1860)
- Add `abi3-py310` feature. [#1889](https://github.com/PyO3/pyo3/pull/1889)
- Add `PyCFunction::new_closure` to create a Python function from a Rust closure. [#1901](https://github.com/PyO3/pyo3/pull/1901)
- Add support for positional-only arguments in `#[pyfunction]` [#1925](https://github.com/PyO3/pyo3/pull/1925)

### Changed

Expand Down
5 changes: 4 additions & 1 deletion guide/src/class.md
Original file line number Diff line number Diff line change
Expand Up @@ -691,6 +691,9 @@ the form of `attr_name="default value"`. Each parameter has to match the method

Each parameter can be one of the following types:

* `"/"`: positional-only arguments separator, each parameter defined before `"/"` is a
positional-only parameter.
Corresponds to python's `def meth(arg1, arg2, ..., /, argN..)`.
* `"*"`: var arguments separator, each parameter defined after `"*"` is a keyword-only parameter.
Corresponds to python's `def meth(*, arg1.., arg2=..)`.
* `args="*"`: "args" is var args, corresponds to Python's `def meth(*args)`. Type of the `args`
Expand Down Expand Up @@ -745,7 +748,7 @@ impl MyClass {
}
}
```
N.B. the position of the `"*"` argument (if included) controls the system of handling positional and keyword arguments. In Python:
N.B. the position of the `"/"` and `"*"` arguments (if included) control the system of handling positional and keyword arguments. In Python:
```python
import mymodule

Expand Down
11 changes: 11 additions & 0 deletions pyo3-macros-backend/src/method.rs
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,17 @@ impl<'a> FnSpec<'a> {
None
}

pub fn is_pos_only(&self, name: &syn::Ident) -> bool {
for s in self.attrs.iter() {
if let Argument::PosOnlyArg(path, _) = s {
if path.is_ident(name) {
return true;
}
}
}
false
}

pub fn is_kw_only(&self, name: &syn::Ident) -> bool {
for s in self.attrs.iter() {
if let Argument::Kwarg(path, _) = s {
Expand Down
8 changes: 6 additions & 2 deletions pyo3-macros-backend/src/params.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ pub fn impl_arg_params(
};

let mut positional_parameter_names = Vec::new();
let mut positional_only_parameters = 0usize;
let mut required_positional_parameters = 0usize;
let mut keyword_only_parameters = Vec::new();

Expand All @@ -86,6 +87,7 @@ pub fn impl_arg_params(
continue;
}
let name = arg.name.unraw().to_string();
let posonly = spec.is_pos_only(arg.name);
let kwonly = spec.is_kw_only(arg.name);
let required = !(arg.optional.is_some() || spec.default_value(arg.name).is_some());

Expand All @@ -100,6 +102,9 @@ pub fn impl_arg_params(
if required {
required_positional_parameters += 1;
}
if posonly {
positional_only_parameters += 1;
aganders3 marked this conversation as resolved.
Show resolved Hide resolved
}
positional_parameter_names.push(name);
}
}
Expand Down Expand Up @@ -154,8 +159,7 @@ pub fn impl_arg_params(
cls_name: #cls_name,
func_name: stringify!(#python_name),
positional_parameter_names: &[#(#positional_parameter_names),*],
// TODO: https://github.com/PyO3/pyo3/issues/1439 - support specifying these
positional_only_parameters: 0,
positional_only_parameters: #positional_only_parameters,
required_positional_parameters: #required_positional_parameters,
keyword_only_parameters: &[#(#keyword_only_parameters),*],
accept_varargs: #accept_args,
Expand Down
28 changes: 27 additions & 1 deletion pyo3-macros-backend/src/pyfunction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@ use syn::{

#[derive(Debug, Clone, PartialEq)]
pub enum Argument {
PosOnlyArgsSeparator,
VarArgsSeparator,
VarArgs(syn::Path),
KeywordArgs(syn::Path),
PosOnlyArg(syn::Path, Option<String>),
Arg(syn::Path, Option<String>),
Kwarg(syn::Path, Option<String>),
}
Expand All @@ -34,6 +36,7 @@ pub enum Argument {
pub struct PyFunctionSignature {
pub arguments: Vec<Argument>,
has_kw: bool,
has_posonly_args: bool,
has_varargs: bool,
has_kwargs: bool,
}
Expand Down Expand Up @@ -126,7 +129,22 @@ impl PyFunctionSignature {
self.arguments.push(Argument::VarArgsSeparator);
Ok(())
}
_ => bail_spanned!(item.span() => "expected \"*\""),
syn::Lit::Str(lits) if lits.value() == "/" => {
// "/"
self.posonly_arg_is_ok(item)?;
self.has_posonly_args = true;
// any arguments _before_ this become positional-only
self.arguments.iter_mut().for_each(|a| {
if let Argument::Arg(path, name) = a {
*a = Argument::PosOnlyArg(path.clone(), name.clone());
} else {
unreachable!();
}
birkenfeld marked this conversation as resolved.
Show resolved Hide resolved
});
self.arguments.push(Argument::PosOnlyArgsSeparator);
Ok(())
}
_ => bail_spanned!(item.span() => "expected \"/\" or \"*\""),
}
}

Expand All @@ -143,6 +161,14 @@ impl PyFunctionSignature {
Ok(())
}

fn posonly_arg_is_ok(&self, item: &NestedMeta) -> syn::Result<()> {
ensure_spanned!(
!(self.has_posonly_args || self.has_kwargs || self.has_varargs),
item.span() => "/ is not allowed after /, varargs(*), or kwargs(**)"
);
Ok(())
}

fn vararg_is_ok(&self, item: &NestedMeta) -> syn::Result<()> {
ensure_spanned!(
!(self.has_kwargs || self.has_varargs),
Expand Down
199 changes: 199 additions & 0 deletions tests/test_methods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,46 @@ impl MethArgs {
[a.to_object(py), args.into(), kwargs.to_object(py)].to_object(py)
}

#[args(a, b, "/")]
fn get_pos_only(&self, a: i32, b: i32) -> i32 {
a + b
}

#[args(a, "/", b)]
fn get_pos_only_and_pos(&self, a: i32, b: i32) -> i32 {
a + b
}

#[args(a, "/", b, c = 5)]
fn get_pos_only_and_pos_and_kw(&self, a: i32, b: i32, c: i32) -> i32 {
a + b + c
}

#[args(a, "/", "*", b)]
fn get_pos_only_and_kw_only(&self, a: i32, b: i32) -> i32 {
a + b
}

#[args(a, "/", "*", b = 3)]
fn get_pos_only_and_kw_only_with_default(&self, a: i32, b: i32) -> i32 {
a + b
}

#[args(a, "/", b, "*", c, d = 5)]
fn get_all_arg_types_together(&self, a: i32, b: i32, c: i32, d: i32) -> i32 {
a + b + c + d
}

#[args(a, "/", args = "*")]
fn get_pos_only_with_varargs(&self, a: i32, args: Vec<i32>) -> i32 {
a + args.iter().sum::<i32>()
}

#[args(a, "/", kwargs = "**")]
fn get_pos_only_with_kwargs(&self, py: Python, a: i32, kwargs: Option<&PyDict>) -> PyObject {
[a.to_object(py), kwargs.to_object(py)].to_object(py)
}

birkenfeld marked this conversation as resolved.
Show resolved Hide resolved
#[args("*", a = 2, b = 3)]
fn get_kwargs_only_with_defaults(&self, a: i32, b: i32) -> i32 {
a + b
Expand Down Expand Up @@ -308,6 +348,165 @@ fn meth_args() {
py_expect_exception!(py, inst, "inst.get_pos_arg_kw(1, a=1)", PyTypeError);
py_expect_exception!(py, inst, "inst.get_pos_arg_kw(b=2)", PyTypeError);

py_run!(py, inst, "assert inst.get_pos_only(10, 11) == 21");
py_expect_exception!(py, inst, "inst.get_pos_only(10, b = 11)", PyTypeError);
py_expect_exception!(py, inst, "inst.get_pos_only(a = 10, b = 11)", PyTypeError);

py_run!(py, inst, "assert inst.get_pos_only_and_pos(10, 11) == 21");
py_run!(
py,
inst,
"assert inst.get_pos_only_and_pos(10, b = 11) == 21"
);
py_expect_exception!(
py,
inst,
"inst.get_pos_only_and_pos(a = 10, b = 11)",
PyTypeError
);

py_run!(
py,
inst,
"assert inst.get_pos_only_and_pos_and_kw(10, 11) == 26"
);
py_run!(
py,
inst,
"assert inst.get_pos_only_and_pos_and_kw(10, b = 11) == 26"
);
py_run!(
py,
inst,
"assert inst.get_pos_only_and_pos_and_kw(10, 11, c = 0) == 21"
);
py_run!(
py,
inst,
"assert inst.get_pos_only_and_pos_and_kw(10, b = 11, c = 0) == 21"
);
py_expect_exception!(
py,
inst,
"inst.get_pos_only_and_pos_and_kw(a = 10, b = 11)",
PyTypeError
);

py_run!(
py,
inst,
"assert inst.get_pos_only_and_kw_only(10, b = 11) == 21"
);
py_expect_exception!(
py,
inst,
"inst.get_pos_only_and_kw_only(10, 11)",
PyTypeError
);
py_expect_exception!(
py,
inst,
"inst.get_pos_only_and_kw_only(a = 10, b = 11)",
PyTypeError
);

py_run!(
py,
inst,
"assert inst.get_pos_only_and_kw_only_with_default(10) == 13"
);
py_run!(
py,
inst,
"assert inst.get_pos_only_and_kw_only_with_default(10, b = 11) == 21"
);
py_expect_exception!(
py,
inst,
"inst.get_pos_only_and_kw_only_with_default(10, 11)",
PyTypeError
);
py_expect_exception!(
py,
inst,
"inst.get_pos_only_and_kw_only_with_default(a = 10, b = 11)",
PyTypeError
);

py_run!(
py,
inst,
"assert inst.get_all_arg_types_together(10, 10, c = 10) == 35"
);
py_run!(
py,
inst,
"assert inst.get_all_arg_types_together(10, 10, c = 10, d = 10) == 40"
);
py_run!(
py,
inst,
"assert inst.get_all_arg_types_together(10, b = 10, c = 10, d = 10) == 40"
);
py_expect_exception!(
py,
inst,
"inst.get_all_arg_types_together(10, 10, 10)",
PyTypeError
);
py_expect_exception!(
py,
inst,
"inst.get_all_arg_types_together(a = 10, b = 10, c = 10)",
PyTypeError
);

py_run!(py, inst, "assert inst.get_pos_only_with_varargs(10) == 10");
py_run!(
py,
inst,
"assert inst.get_pos_only_with_varargs(10, 10) == 20"
);
py_run!(
py,
inst,
"assert inst.get_pos_only_with_varargs(10, 10, 10, 10, 10) == 50"
);
py_expect_exception!(
py,
inst,
"inst.get_pos_only_with_varargs(a = 10)",
PyTypeError
);

py_run!(
py,
inst,
"assert inst.get_pos_only_with_kwargs(10) == [10, None]"
);
py_run!(
py,
inst,
"assert inst.get_pos_only_with_kwargs(10, b = 10) == [10, {'b': 10}]"
);
py_run!(
py,
inst,
"assert inst.get_pos_only_with_kwargs(10, b = 10, c = 10, d = 10, e = 10) == [10, {'b': 10, 'c': 10, 'd': 10, 'e': 10}]"
);
py_expect_exception!(
py,
inst,
"inst.get_pos_only_with_kwargs(a = 10)",
PyTypeError
);
py_expect_exception!(
py,
inst,
"inst.get_pos_only_with_kwargs(a = 10, b = 10)",
PyTypeError
);

py_run!(py, inst, "assert inst.get_kwargs_only_with_defaults() == 5");
py_run!(
py,
Expand Down
20 changes: 20 additions & 0 deletions tests/ui/invalid_macro_args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,24 @@ fn kw_after_kwargs(py: Python, kwargs: &PyDict, a: i32) -> PyObject {
[a.to_object(py), vararg.into()].to_object(py)
}

#[pyfunction(a, "*", b, "/", c)]
fn pos_only_after_kw_only(py: Python, a: i32, b: i32, c: i32) -> i32 {
a + b + c
}

#[pyfunction(a, args="*", "/", b)]
fn pos_only_after_args(py: Python, a: i32, args: Vec<i32>, b: i32) -> i32 {
a + b + c
}

#[pyfunction(a, kwargs="**", "/", b)]
fn pos_only_after_kwargs(py: Python, a: i32, args: Vec<i32>, b: i32) -> i32 {
a + b
}

#[pyfunction(kwargs = "**", "*", a)]
fn kw_only_after_kwargs(py: Python, kwargs: &PyDict, a: i32) -> PyObject {
[a.to_object(py), vararg.into()].to_object(py)
}

fn main() {}
Loading