Skip to content

Allow kw-only args after a py::args #3402

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 3 commits into from
Oct 29, 2021
Merged

Conversation

jagerman
Copy link
Member

Description

This removes the constraint that py::args has to be last (or second-last, with py::kwargs) and instead makes py::args imply py::kw_only for any remaining arguments, allowing you to bind a function that works the same way as a Python function such as:

def f(a, *args, b):
    return a * b + sum(args)

f(10, 1, 2, 3, b=20)  # == 206

With this PR you can now bind such a function using pybind11:

    m.def("f", [](int a, py::args args, int b) { /* ... */ },
        "a"_a, "b"_a);

Or, to be more explicit about the keyword-only arguments:

    m.def("g", [](int a, py::args args, int b) { /* ... */ },
        "a"_a, py::kw_only{}, "b"_a);

The only difference between the two is that the latter will fail at binding time if the py::kw_only{} position doesn't match the relative position of the py::args argument.

(This doesn't affect backwards compatibility at all because, currently, you can't have a py::args anywhere except the end/2nd-last.)

Suggested changelog entry:

* Allow ``py::args`` to be followed by other arguments; the remaining
  arguments are implicitly keyword-only, as if a `py::kw_only{}` annotation
  had been used.

This simplifies tracking the number of kw-only args by instead tracking
the number of positional arguments (which is really what we care about
everywhere this is used).
@jagerman jagerman force-pushed the kwonly-after-args branch 2 times, most recently from 22c4fe0 to 751f176 Compare October 24, 2021 13:21
This removes the constraint that py::args has to be last (or
second-last, with py::kwargs) and instead makes py::args imply
py::kw_only for any remaining arguments, allowing you to bind a function
that works the same way as a Python function such as:

    def f(a, *args, b):
        return a * b + sum(args)

    f(10, 1, 2, 3, b=20)  # == 206

With this change, you can bind such a function using:

    m.def("f", [](int a, py::args args, int b) { /* ... */ },
        "a"_a, "b"_a);

Or, to be more explicit about the keyword-only arguments:

    m.def("g", [](int a, py::args args, int b) { /* ... */ },
        "a"_a, py::kw_only{}, "b"_a);

(The only difference between the two is that the latter will fail at
binding time if the `kw_only{}` doesn't match the `py::args` position).

This doesn't affect backwards compatibility at all because, currently,
you can't have a py::args anywhere except the end/2nd-last.
@@ -56,6 +56,23 @@ TEST_SUBMODULE(kwargs_and_defaults, m) {
m.def("mixed_plus_args_kwargs_defaults", mixed_plus_both,
py::arg("i") = 1, py::arg("j") = 3.14159);

m.def("args_kwonly",
Copy link
Collaborator

@Skylion007 Skylion007 Oct 24, 2021

Choose a reason for hiding this comment

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

These tests should all pass py::args and py::kwargs by ref through the lambda since they are not trivially copyable. (This is the clang-tidy rule that fails).

@henryiii
Copy link
Collaborator

Let's try to get the last few fixes in to 2.8.1, then merge this (also okay to branch, but since we are very close to a patch release, a few day delay is probably fine). Other than the tiny test fix, this sounds like a great idea.

@Skylion007
Copy link
Collaborator

It surprises me that py::args is not trivially copyable.

@henryiii
Copy link
Collaborator

henryiii commented Oct 28, 2021

py::args is a py::tuple, which is a py::object, and copying a py::object increases the reference count, so it's not trivially copyable.

@henryiii
Copy link
Collaborator

Thanks for doing this! It was something I thought about briefly when adding pos_only and touching up kw_only, but never came back to.

You've mixed MD and RST syntax in the changelog entry above. ;) I think we need to bite the bullet and make the switch to MD for the changelog. Soon.

@henryiii henryiii merged commit e7c9753 into pybind:master Oct 29, 2021
@github-actions github-actions bot added the needs changelog Possibly needs a changelog entry label Oct 29, 2021
@jagerman
Copy link
Member Author

py::args is a py::tuple, which is a py::object, and copying a py::object increases the reference count, so it's not trivially copyable.

Though considering that in order to touch any of this you have to hold the GIL, and thus the reference count doesn't even have to be atomic, it's fairly "trivial" in practice, just not in the C++ sense of the word.

@rwgk
Copy link
Collaborator

rwgk commented Nov 8, 2021

Just a quick note at the moment (I need to find time to debug): I'm seeing test breakages in Google's global testing that may be related to this PR.

https://github.com/google/tensorstore

TypeError: __init__(): incompatible constructor arguments. The following argument types are supported:
    1. tensorstore.IndexTransform(input_rank: Optional[int] = None, *, input_inclusive_min: Optional[Sequence[int]] = None, implicit_lower_bounds: Optional[Sequence[bool]] = None, input_exclusive_max: Optional[Sequence[int]] = None, input_inclusive_max: Optional[Sequence[int]] = None, input_shape: Optional[Sequence[int]] = None, implicit_upper_bounds: Optional[Sequence[bool]] = None, input_labels: Optional[Sequence[Optional[str]]] = None, output: Optional[Sequence[tensorstore.OutputIndexMap]] = None)
    2. tensorstore.IndexTransform(domain: tensorstore.IndexDomain, output: Optional[Sequence[tensorstore.OutputIndexMap]] = None)
    3. (*, self: tensorstore.IndexTransform, json: Any) -> None

Invoked with: kwargs: json={'input_exclusive_max': [2, 3], 'input_inclusive_min': [0, 0], 'input_labels': ['x', 'y']}
def test_translate_by_vector():
      x = tstf.IndexTransform(input_shape=[2, 3], input_labels=["x", "y"])
>     check_dim_expr(x, lambda d: d["x", "y"].translate_by[4, 5])

third_party/py/tensorstore/tensorflow/tests/dim_expression_test.py:54: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
third_party/py/tensorstore/tensorflow/tests/dim_expression_test.py:43: in check_dim_expr
    ts_transform = transform.to_pyval()
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <[TypeError("__init__(): incompatible constructor arguments. The following argument types are supported:\n    1. tenso...'input_inclusive_min': [0, 0], 'input_labels': ['x', 'y']}") raised in repr()] IndexTransform object at 0x7f03a893db90>

    def to_pyval(self) -> tensorstore.IndexTransform:
      """Returns an equivalent `tensorstore.IndexTransform.
    
      This is only supported under eager execution mode.
      """
      if not tf.executing_eagerly():
        raise NotImplementedError(
            'IndexTransform.to_pyval only supported during eager execution')
>     return tensorstore.IndexTransform(json=self.to_json())
E     TypeError: __init__(): incompatible constructor arguments. The following argument types are supported:
E         1. tensorstore.IndexTransform(input_rank: Optional[int] = None, *, input_inclusive_min: Optional[Sequence[int]] = None, implicit_lower_bounds: Optional[Sequence[bool]] = None, input_exclusive_max: Optional[Sequence[int]] = None, input_inclusive_max: Optional[Sequence[int]] = None, input_shape: Optional[Sequence[int]] = None, implicit_upper_bounds: Optional[Sequence[bool]] = None, input_labels: Optional[Sequence[Optional[str]]] = None, output: Optional[Sequence[tensorstore.OutputIndexMap]] = None)
E         2. tensorstore.IndexTransform(domain: tensorstore.IndexDomain, output: Optional[Sequence[tensorstore.OutputIndexMap]] = None)
E         3. (*, self: tensorstore.IndexTransform, json: Any) -> None
E     
E     Invoked with: kwargs: json={'input_exclusive_max': [2, 3], 'input_inclusive_min': [0, 0], 'input_labels': ['x', 'y']}

third_party/py/tensorstore/tensorflow/index_transform.py:926: TypeError

@henryiii
Copy link
Collaborator

henryiii commented Nov 8, 2021

Here's where the defs are: https://github.com/google/tensorstore/blob/1a59fcb310bc1feb13569f03f7134b4c3a5fa5f4/python/tensorstore/index_space.cc#L823

My guess is the logic for computing these is off (the *, self looks really off to me), and we don't have a properly complex test case for mixing these types of args. Just a guess, though.

@rwgk
Copy link
Collaborator

rwgk commented Nov 13, 2021

Hi @jagerman, do you think you'll have time to look into this soon?

As-is, I'm blocked from updating pybind11 for Google-internal use (smart_holder branch), which could turn into a secondary problem for OpenSpiel, which is using the smart_holder branch internally and externally.

rwgk added a commit to rwgk/pybind11 that referenced this pull request Nov 18, 2021
rwgk added a commit to rwgk/pybind11 that referenced this pull request Nov 19, 2021
@henryiii henryiii removed the needs changelog Possibly needs a changelog entry label Dec 21, 2021
kljohann added a commit to kljohann/genpybind that referenced this pull request Oct 20, 2024
kljohann added a commit to kljohann/genpybind that referenced this pull request Oct 20, 2024
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.

4 participants