-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Expand float and complex strict mode to allow ints and ints/float (for PEP 484 compatibility). #5879
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
base: master
Are you sure you want to change the base?
Conversation
|
I have started working on this too. |
This is known at compile time so it can be constexpr
|
Here is my implementation and documentation changes. It needs tests but I am going to stop for today. |
|
You will need to remove the constexpr check. I didn't realise it was a newer C++ feature |
Signed-off-by: Michael Carlstrom <rmc@carlstrom.com>
|
TODO: figure out why it doesn't work in C++11. |
Signed-off-by: Michael Carlstrom <rmc@carlstrom.com>
|
I have just found that a python float passed to an int argument will raise a TypeError. This is probably something we should resolve here because it effects these tests. m.def("func", [](int){});>>> func.__doc__
func(arg: typing.SupportsInt) -> None
>>> func(5.5)
TypeError
>>> func(numpy.float32(5.5)) # This works fine |
The int type caster allows anything that implements __int__ with explicit exception of the python float. I can't see any reason for this. This modifies the int casting behaviour to accept a float. If the argument is marked as noconvert() it will only accept int.
|
Here is my proposed change. You will need to update the tests. Here are my unittests |
|
I noticed something else to have address. According to the documentation all of int,float,complex support falling back on |
|
That's much easier to review now! The production code changes are actually very easy to review now. I'll comb through the test changes carefully tomorrow, for a final confirmation that everything is OK. I want to hold back merging though. The plan I have in mind:
|
|
xref regarding merge & release planning: #5887 (comment) |
Replace cryptic 'So: PYBIND11_INDEX_CHECK(src.ptr())' comment with clearer explanation of the logic: - Explains that we need to call PyNumber_Index explicitly on PyPy for non-PyLong objects - Clarifies the relationship to the outer condition: when convert is false, we only reach this point if PYBIND11_INDEX_CHECK passed above This makes the code more maintainable and easier to understand during review.
During merge, HEAD's regex pattern was kept, but master's version is preferred. The order of ` ` and `\|` in the character class is arbitrary. Keep master's order (already fixed in PR pybind#5891; sorry I missed looking back here when working on 5891).
…1)` call and clearly explain the behavior change.
Enabling implicit float → int conversion in convert mode causes silent truncation (e.g., 1.9 → 1). This is dangerous because: 1. It's implicit - users don't expect truncation when calling functions 2. It's silent - no warning or error 3. It can hide bugs - precision loss is hard to detect This change restores the explicit rejection of PyFloat_Check for integer casters, even in convert mode. This is more in line with Python's behavior where int(1.9) must be explicit. Note that the int → float conversion in noconvert mode is preserved, as that's a safe widening conversion.
This reverts all test modifications that were made to accommodate implicit float→int conversion in convert mode. With the production code change that explicitly rejects float→int conversion even in convert mode, these test workarounds are no longer needed. Changes reverted: - test_builtin_casters.py: Restored cant_convert(3.14159) and np.float32 conversion with deprecated_call wrapper - test_custom_type_casters.py: Restored TypeError expectation for m.ints_preferred(4.0) - test_methods_and_attributes.py: Restored TypeError expectation for m.overload_order(1.1) - test_stl.py: Restored float literals (2.0) that were replaced with strings to avoid conversion - test_factory_constructors.py: Restored original constructor calls that were modified to avoid float→int conversion Also removes the unused avoid_PyLong_AsLong_deprecation fixture and related TypeVar imports, as all uses were removed.
The env.deprecated_call() function was removed, but two test cases still reference it. Replace with pytest.deprecated_call(), which is the standard pytest context manager for handling deprecation warnings. Since we already require pytest>=6 (see tests/requirements.txt), the compatibility function is obsolete and pytest.deprecated_call() is available.
PR 5879 swapped the order of NoisyAlloc constructor overloads: - (int i, double) is now placement new (comes first) - (double d, double) is now factory pointer (comes second) This swap is necessary because pybind11 tries overloads in order until one matches. With int → float conversion now allowed: - create_and_destroy(4, 0.5): Without the swap, (double d, double) would match first (since int → double conversion is allowed), bypassing the more specific (int i, double) overload. With the swap, (int i, double) matches first (exact match), which is correct. - create_and_destroy(3.5, 4.5): (int i, double) fails (float → int is rejected), then (double d, double) matches, which is correct. The swap ensures exact int matches are preferred over double matches when an int is provided, which is the expected overload resolution behavior. Update the test expectations to match the new overload resolution order.
|
I've been working through the PR to understand the behavior changes and ensure everything is correct. Sorry it took me way longer than I'd want to admit to realize that this PR was enabling implicit, narrowing Python float to C++ int conversions. Here is where I realized: After the penny dropped, I added this commit:
Please see the comprehensive commit message for the rationale. These are follow-on commits to make the tests pass again:
Before that I added a couple minor commits:
There are still things I want to look into [EDIT: DONE], to make sure we're not creating weird oddities (beyond already existing oddities):
I probably won't have a block of time to focus on this PR again before next weekend, but I'll read all comments. Note: I've been using Cursor a lot, in case you wonder about all the long commit messages. |
Since we already require pytest>=6 (see tests/requirements.txt), the old compatibility function is obsolete and pytest.deprecated_call() can be used directly. Extracted from PR pybind#5879 Co-authored-by: Michael Carlstrom <rmc@carlstrom.com> Co-authored-by: gentlegiantJGC <gentlegiantJGC@users.noreply.github.com>
Since we already require pytest>=6 (see tests/requirements.txt), the old compatibility function is obsolete and pytest.deprecated_call() can be used directly. Extracted from PR pybind#5879 Co-authored-by: Michael Carlstrom <rmc@carlstrom.com> Co-authored-by: gentlegiantJGC <gentlegiantJGC@users.noreply.github.com>
|
FYI — While I was at it, I isolated out this change, to be merged asap: I'll mark you a co-authors there. (I should have done that yesterday for the other PR, too, but sorry it missed the opportunity.) The goal is to make this critical PR as clean and lean as possible. I expect that it will be looked back at a lot, b/o the behavior change. I'll convert the branch to a PR when the CI here is done. (We only have the free GHA quota now, unfortunately.) EDIT: That's PR #5893 now. |
typing.SupportsIndexSince we already require pytest>=6 (see tests/requirements.txt), the old compatibility function is obsolete and pytest.deprecated_call() can be used directly. Extracted from PR #5879 Co-authored-by: Michael Carlstrom <rmc@carlstrom.com> Co-authored-by: gentlegiantJGC <gentlegiantJGC@users.noreply.github.com>
/__w/pybind11/pybind11/include/pybind11/cast.h:253:46: error: repeated branch body in conditional chain [bugprone-branch-clone,-warnings-as-errors]
253 | } else if (PyFloat_Check(src.ptr())) {
| ^
/__w/pybind11/pybind11/include/pybind11/cast.h:258:10: note: end of the original
258 | } else if (convert || PYBIND11_LONG_CHECK(src.ptr()) || PYBIND11_INDEX_CHECK(src.ptr())) {
| ^
/__w/pybind11/pybind11/include/pybind11/cast.h:283:16: note: clone 1 starts here
283 | } else {
| ^
gentlegiantJGC
left a comment
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.
I can't see any obvious issues
|
I will open a new issue about the custom float -> int conversion |
…eturning float These tests ensure that: - Invalid return types (floats) are properly rejected - The fallback from __index__ to __int__ works correctly in convert mode - noconvert mode correctly prevents fallback when __index__ fails
|
The extra tests (commit f1d8158) are:
This is a bit on the paranoid side, but I wanted to reassure myself that we're not putting the world through a behavior change without having looked at every dark corner. |
|
BTW, having looked at this so intensely ... bool py_err = py_value == (py_type) -1 && PyErr_Occurred();this will be OK if Python is healthy, but if something goes wrong (e.g. UB, or bug in Python beta testing) and So that condition basically errs on the unsafe side, it prioritizes the return value check over error detection. I think it'd be better to write: bool py_err = PyErr_Occurred();
if (py_err) assert(py_value == (py_type) -1);This prioritizes error detection over return value validation. WDYT? |
…ere (e.g. UB, or bug in Python beta testing). See also: pybind#5879 (comment)
Same. I looked through everything several times now. Thanks! |
Description
Breaking change to allow Python ints into float when under strict typing (
noconvert), to better match PEP 484 numeric tower rules.This change was also done to complex to allow floats and ints. Now that Python ints can be passed into floats it changes behavior for overload resolution. A method that takes float that is registered before a method that takes an int will now get executed when a Python int is passed in. This overload resolution also affects methods with
std::complex.Resolves #5878
Provides a work around for #5767
NOTE: This PR was reviewed extensively and is approved. However, because of the breaking change, it will be merged only after the next patch release.
Suggested changelog entry:
Breaking change for PEP 484 compatibility: Expand float and complex strict mode to allow ints and ints/float.
📚 Documentation preview 📚: https://pybind11--5879.org.readthedocs.build/