-
Notifications
You must be signed in to change notification settings - Fork 2.1k
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
feat(typing): allow annotate methods with pos_only
when only have the self
argument
#5403
Conversation
9cd2ff6
to
567000e
Compare
93cc71f
to
fa887bd
Compare
Could you please explain in the PR description why this is beneficial? |
We generate the help message and function signature from the method definition. >>> import pybind_module
>>> help(pybind_module.MyClass)
>>> print(pybind_module.MyClass.some_method.__doc__) For those methods are not explicitly defined by the users, they will use the definition from CPython: E.g.: TPSLOT(__repr__, tp_repr, slot_tp_repr, wrap_unaryfunc,
"__repr__($self, /)\n--\n\nReturn repr(self)."),
TPSLOT(__hash__, tp_hash, slot_tp_hash, wrap_hashfunc,
"__hash__($self, /)\n--\n\nReturn hash(self)."),
TPSLOT(__str__, tp_str, slot_tp_str, wrap_unaryfunc,
"__str__($self, /)\n--\n\nReturn str(self)."),
TPSLOT(__getattr__, tp_getattro, _Py_slot_tp_getattr_hook, NULL,
"__getattr__($self, name, /)\n--\n\nImplement getattr(self, name)."),
TPSLOT(__setattr__, tp_setattro, slot_tp_setattro, wrap_setattr,
"__setattr__($self, name, value, /)\n--\n\nImplement setattr(self, name, value)."),
TPSLOT(__delattr__, tp_setattro, slot_tp_setattro, wrap_delattr,
"__delattr__($self, name, /)\n--\n\nImplement delattr(self, name)."), Their arguments are positional-only. Before this PR, if the user defines >>> help(pybind_module.MyClass)
| ...
|
| __repr__(...)
| __repr__(self: pybind_module.MyClass) -> str # <--- generated by pybind11: not positional-only
|
| Return a string representation of pybind_module.MyClass.
|
| __str__(self, /) # <--- from cpython/Objects/typeobject.c: positional-only
| Return str(self). |
Thanks. So is this PR to make the pybind11 behavior compatible with standard Python behavior? Is this only for cosmetic reasons, or do you see failures in some context/environment without this PR? I will need some time to fully understand the
? |
Yes. The previous
Before this PR (the current py::class_<MyClass>(...)
.def("__repr__", &MyClass::ToString, "Return a string representation.", py::pos_only())
...; The compilation fails: pybind11/include/pybind11/pybind11.h:306:27: error: static assertion failed due to requirement 'has_arg_annotations || !has_pos_only_args': py::pos_only requires the use of argument annotations (for docstrings and aligning the annotations to the argument)
306 | static_assert(has_arg_annotations || !has_pos_only_args,
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
pybind11/include/pybind11/pybind11.h:157:9: note: in instantiation of function template specialization 'pybind11::cpp_function::initialize<(lambda at pybind11/include/pybind11/pybind11.h:157:20), std::string, const MyClass *, pybind11::name, pybind11::is_method, pybind11::sibling, char[32], pybind11::pos_only>' requested here
157 | initialize([f](const Class *c,
| ^
pybind11/include/pybind11/pybind11.h:1621:22: note: in instantiation of function template specialization 'pybind11::cpp_function::cpp_function<std::string, MyClass, pybind11::name, pybind11::is_method, pybind11::sibling, char[32], pybind11::pos_only>' requested here
1621 | cpp_function cf(method_adaptor<type>(std::forward<Func>(f)),
| ^
myclass.cpp:342:10: note: in instantiation of function template specialization 'pybind11::class_<MyClass>::def<std::string (MyClass::*)() const, char[32], pybind11::pos_only>' requested here
342 | .def("__repr__", &MyClass::ToString, "Return a string representation.", py::pos_only()) Because the
For functions, this PR does not change any behavior other than the current // before and after this PR
// signature: my_function(/) # this is also not allowed in Python
mod.def("my_function", py::pos_only()) // fail(expected): no annotated argument
// signature: my_function(x, /)
mod.def("my_function", py::arg("x"), py::pos_only()) // OK
// signature: my_function(x, /, y)
mod.def("my_function", py::arg("x"), py::pos_only(), py::arg("y")) // OK For methods, it allows a standalone // before this PR
// signature: my_method(self, /) # allowed in Python
cls.def("my_method", py::pos_only()) // fail(unexpected): no annotated argument
// (this is not correct because there is
// an implicitly added `self` argument
// automatically added in `py::class_.def(...)`
// (which set `py::is_method`))
// signature: my_method(self, x, /)
cls.def("my_method", py::arg("x"), py::pos_only()) // OK
// signature: my_method(self, x, /, y)
cls.def("my_method", py::arg("x"), py::pos_only(), py::arg("y")) // OK
// after this PR
// signature: my_method(self, /) # allowed in Python
cls.def("my_method", py::pos_only()) // OK
// signature: my_method(self, x, /)
cls.def("my_method", py::arg("x"), py::pos_only()) // OK
// signature: my_method(self, x, /, y)
cls.def("my_method", py::arg("x"), py::pos_only(), py::arg("y")) // OK |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for 09a4992, that makes it pretty easy to follow the logic.
Did you consider making pos_only
the default for the is_method_with_self_arg_only
case? Then we (and the rest of the world) wouldn't need all the changes adding pos_only()
explicitly.
I'd like to leave this decision to the users. They should explicitly add In [1]: class Foo:
...: def foo(self):
...: print('Hello')
...:
In [2]: obj = Foo()
In [3]: obj.foo()
Hello
In [4]: Foo.foo(obj)
Hello
In [5]: Foo.foo(self=obj)
Hello In this PR, I add |
I added a test (34e808f), in part to verify my understanding that adding
OK, I agree after thinking about it more. I strongly agree that changing the I'm less certain about the rest of the changes, adding Anyway, that's a weird/unfortunate/artificial problem, and Google has ways to deal with it, so I'd support making the change here. If you want to keep the changes for the dunder methods, could you please add unit tests to cover all changes you're making, by adding asserts for Sorry it's a bit of a chore to figure out where to insert those tests, but it's important to have them for long-term stability. (To pin-point where to add the tests, I'd try to intentionally break one changed dunder method at a time, then run all existing unit tests locally.) |
BTW Earlier I wrote:
What I had in mind: Did you see failures that forced you to add (I realized before that the |
Yes, it is a breaking change for generated dunder methods. For user-defined methods, it is an opt-in feature.
No. I have tested with the latest pip3 install -U mypy
pip3 install -e tests
cd tests
stubgen -m pybind11_tests.enums class UnscopedEnum:
__members__: ClassVar[dict] = ... # read-only
EOne: ClassVar[UnscopedEnum] = ...
EThree: ClassVar[UnscopedEnum] = ...
ETwo: ClassVar[UnscopedEnum] = ...
__entries: ClassVar[dict] = ...
def __init__(self, value: int) -> None: ...
def __and__(self, other): ...
def __eq__(self, other: object) -> bool: ...
def __ge__(self, other: object) -> bool: ...
def __gt__(self, other: object) -> bool: ...
def __hash__(self) -> int: ...
def __index__(self) -> int: ...
def __int__(self) -> int: ...
def __invert__(self): ...
def __le__(self, other: object) -> bool: ...
def __lt__(self, other: object) -> bool: ...
def __ne__(self, other: object) -> bool: ...
def __or__(self, other): ...
def __rand__(self, other): ...
def __ror__(self, other): ...
def __rxor__(self, other): ...
def __xor__(self, other): ...
@property
def name(self): ...
@property
def value(self) -> int: ... In the generated stub file, no argument (including unary and binary methods) is marked as pos-only.
I have added corresponding tests for each change in the latest commit in this PR.
There was no failure that forced me to add
As commented in #5403 (comment), just for consistency with the CPython source code and the Python code where I'm working on a PR to drop Python 3.7 and enable Python 3.8 features for my C extension. I got failures as commented in #5403 (comment). |
07c7c7d
to
601e499
Compare
41dccd4
to
ebaf3c8
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks a lot for the thorough testing!
@@ -198,10 +198,11 @@ def pytest_assertrepr_compare(op, left, right): # noqa: ARG001 | |||
|
|||
|
|||
def gc_collect(): | |||
"""Run the garbage collector twice (needed when running | |||
"""Run the garbage collector three times (needed when running |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, I didn't realize we had this here. Skipping for PyPy (and GraalPy I guess) would be better. But for later, another PR, maybe.
Out of curiosity, is there any version that this patch targeted? I tried to enable this feature in my source code with a version guard. #if PYBIND11_VERSION_HEX >= 0x020E00F0 // pybind11 2.14.0
#define def_method_pos_only(...) def(__VA_ARGS__ __VA_OPT__(, ) py::pos_only())
#else
#define def_method_pos_only(...) def(__VA_ARGS__)
#endif I'm not sure if this is the correct version that my code will not break in the future. |
Description
This PR allows to annotate methods such as
__repr__(self)
to be positional-only. For example:will generate annotation:
Previously, the compilation failed here:
pybind11/include/pybind11/pybind11.h
Lines 301 to 308 in af67e87
Although there is an implicitly defined argument
self
not present bypy::arg("self")
.Suggested changelog entry: