Skip to content

Commit 9f1187f

Browse files
authored
Add typing.SupportsIndex to int/float/complex type hints (#5891)
* Add typing.SupportsIndex to int/float/complex type hints This corrects a mistake where these types were supported but the type hint was not updated to reflect that SupportsIndex objects are accepted. To track the resulting test failures: The output of "$(cat PYROOT)"/bin/python3 $HOME/clone/pybind11_scons/run_tests.py $HOME/forked/pybind11 -v is in ~/logs/pybind11_pr5879_scons_run_tests_v_log_2025-11-10+122217.txt * Cursor auto-fixes (partial) plus pre-commit cleanup. 7 test failures left to do. * Fix remaining test failures, partially done by cursor, partially manually. * Cursor-generated commit: Added the Index() tests from PR 5879. Summary: Changes Made 1. **C++ Bindings** (`tests/test_builtin_casters.cpp`) • Added complex_convert and complex_noconvert functions needed for the tests 2. **Python Tests** (`tests/test_builtin_casters.py`) `test_float_convert`: • Added Index class with __index__ returning -7 • Added Int class with __int__ returning -5 • Added test showing Index() works with convert mode: assert pytest.approx(convert(Index())) == -7.0 • Added test showing Index() doesn't work with noconvert mode: requires_conversion(Index()) • Added additional assertions for int literals and Int() class `test_complex_cast`: • Expanded the test to include convert and noconvert functionality • Added Index, Complex, Float, and Int classes • Added test showing Index() works with convert mode: assert convert(Index()) == 1 and assert isinstance(convert(Index()), complex) • Added test showing Index() doesn't work with noconvert mode: requires_conversion(Index()) • Added type hint assertions matching the SupportsIndex additions These tests demonstrate that custom __index__ objects work with float and complex in convert mode, matching the typing.SupportsIndex type hint added in PR 5891. * Reflect behavior changes going back from PR 5879 to master. This diff will have to be reapplied under PR 5879. * Add PyPy-specific __index__ handling for complex caster Extract PyPy-specific __index__ backporting from PR 5879 to fix PyPy 3.10 test failures in PR 5891. This adds: 1. PYBIND11_INDEX_CHECK macro in detail/common.h: - Uses PyIndex_Check on CPython - Uses hasattr check on PyPy (workaround for PyPy 7.3.3 behavior) 2. PyPy-specific __index__ handling in complex.h: - Handles __index__ objects on PyPy 7.3.7's 3.8 which doesn't implement PyLong_*'s __index__ calls - Mirrors the logic used in numeric_caster for ints and floats This backports __index__ handling for PyPy, matching the approach used in PR 5879's expand-float-strict branch.
1 parent 73da78c commit 9f1187f

18 files changed

+204
-80
lines changed

include/pybind11/cast.h

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -347,9 +347,12 @@ struct type_caster<T, enable_if_t<std::is_arithmetic<T>::value && !is_std_char_t
347347
return PyLong_FromUnsignedLongLong((unsigned long long) src);
348348
}
349349

350-
PYBIND11_TYPE_CASTER(T,
351-
io_name<std::is_integral<T>::value>(
352-
"typing.SupportsInt", "int", "typing.SupportsFloat", "float"));
350+
PYBIND11_TYPE_CASTER(
351+
T,
352+
io_name<std::is_integral<T>::value>("typing.SupportsInt | typing.SupportsIndex",
353+
"int",
354+
"typing.SupportsFloat | typing.SupportsIndex",
355+
"float"));
353356
};
354357

355358
template <typename T>

include/pybind11/complex.h

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,23 @@ class type_caster<std::complex<T>> {
5454
if (!convert && !PyComplex_Check(src.ptr())) {
5555
return false;
5656
}
57-
Py_complex result = PyComplex_AsCComplex(src.ptr());
57+
handle src_or_index = src;
58+
// PyPy: 7.3.7's 3.8 does not implement PyLong_*'s __index__ calls.
59+
// The same logic is used in numeric_caster for ints and floats
60+
#if defined(PYPY_VERSION)
61+
object index;
62+
if (PYBIND11_INDEX_CHECK(src.ptr())) {
63+
index = reinterpret_steal<object>(PyNumber_Index(src.ptr()));
64+
if (!index) {
65+
PyErr_Clear();
66+
if (!convert)
67+
return false;
68+
} else {
69+
src_or_index = index;
70+
}
71+
}
72+
#endif
73+
Py_complex result = PyComplex_AsCComplex(src_or_index.ptr());
5874
if (result.real == -1.0 && PyErr_Occurred()) {
5975
PyErr_Clear();
6076
return false;
@@ -68,7 +84,10 @@ class type_caster<std::complex<T>> {
6884
return PyComplex_FromDoubles((double) src.real(), (double) src.imag());
6985
}
7086

71-
PYBIND11_TYPE_CASTER(std::complex<T>, const_name("complex"));
87+
PYBIND11_TYPE_CASTER(
88+
std::complex<T>,
89+
io_name("typing.SupportsComplex | typing.SupportsFloat | typing.SupportsIndex",
90+
"complex"));
7291
};
7392
PYBIND11_NAMESPACE_END(detail)
7493
PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE)

include/pybind11/detail/common.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,13 @@
322322
#define PYBIND11_BYTES_AS_STRING PyBytes_AsString
323323
#define PYBIND11_BYTES_SIZE PyBytes_Size
324324
#define PYBIND11_LONG_CHECK(o) PyLong_Check(o)
325+
// In PyPy 7.3.3, `PyIndex_Check` is implemented by calling `__index__`,
326+
// while CPython only considers the existence of `nb_index`/`__index__`.
327+
#if !defined(PYPY_VERSION)
328+
# define PYBIND11_INDEX_CHECK(o) PyIndex_Check(o)
329+
#else
330+
# define PYBIND11_INDEX_CHECK(o) hasattr(o, "__index__")
331+
#endif
325332
#define PYBIND11_LONG_AS_LONGLONG(o) PyLong_AsLongLong(o)
326333
#define PYBIND11_LONG_FROM_SIGNED(o) PyLong_FromSsize_t((ssize_t) (o))
327334
#define PYBIND11_LONG_FROM_UNSIGNED(o) PyLong_FromSize_t((size_t) (o))

tests/test_builtin_casters.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,8 @@ TEST_SUBMODULE(builtin_casters, m) {
363363
m.def("complex_cast", [](float x) { return "{}"_s.format(x); });
364364
m.def("complex_cast",
365365
[](std::complex<float> x) { return "({}, {})"_s.format(x.real(), x.imag()); });
366+
m.def("complex_convert", [](std::complex<float> x) { return x; });
367+
m.def("complex_noconvert", [](std::complex<float> x) { return x; }, py::arg{}.noconvert());
366368

367369
// test int vs. long (Python 2)
368370
m.def("int_cast", []() { return (int) 42; });

tests/test_builtin_casters.py

Lines changed: 77 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,10 @@ def __int__(self):
286286

287287
convert, noconvert = m.int_passthrough, m.int_passthrough_noconvert
288288

289-
assert doc(convert) == "int_passthrough(arg0: typing.SupportsInt) -> int"
289+
assert (
290+
doc(convert)
291+
== "int_passthrough(arg0: typing.SupportsInt | typing.SupportsIndex) -> int"
292+
)
290293
assert doc(noconvert) == "int_passthrough_noconvert(arg0: int) -> int"
291294

292295
def requires_conversion(v):
@@ -322,19 +325,39 @@ def cant_convert(v):
322325

323326

324327
def test_float_convert(doc):
328+
class Int:
329+
def __int__(self):
330+
return -5
331+
332+
class Index:
333+
def __index__(self) -> int:
334+
return -7
335+
325336
class Float:
326337
def __float__(self):
327338
return 41.45
328339

329340
convert, noconvert = m.float_passthrough, m.float_passthrough_noconvert
330-
assert doc(convert) == "float_passthrough(arg0: typing.SupportsFloat) -> float"
341+
assert (
342+
doc(convert)
343+
== "float_passthrough(arg0: typing.SupportsFloat | typing.SupportsIndex) -> float"
344+
)
331345
assert doc(noconvert) == "float_passthrough_noconvert(arg0: float) -> float"
332346

333347
def requires_conversion(v):
334348
pytest.raises(TypeError, noconvert, v)
335349

350+
def cant_convert(v):
351+
pytest.raises(TypeError, convert, v)
352+
336353
requires_conversion(Float())
354+
requires_conversion(Index())
337355
assert pytest.approx(convert(Float())) == 41.45
356+
assert pytest.approx(convert(Index())) == -7.0
357+
assert isinstance(convert(Float()), float)
358+
assert pytest.approx(convert(3)) == 3.0
359+
requires_conversion(3)
360+
cant_convert(Int())
338361

339362

340363
def test_numpy_int_convert():
@@ -381,7 +404,7 @@ def test_tuple(doc):
381404
assert (
382405
doc(m.tuple_passthrough)
383406
== """
384-
tuple_passthrough(arg0: tuple[bool, str, typing.SupportsInt]) -> tuple[int, str, bool]
407+
tuple_passthrough(arg0: tuple[bool, str, typing.SupportsInt | typing.SupportsIndex]) -> tuple[int, str, bool]
385408
386409
Return a triple in reversed order
387410
"""
@@ -458,11 +481,61 @@ def test_reference_wrapper():
458481
assert m.refwrap_call_iiw(IncType(10), m.refwrap_iiw) == [10, 10, 10, 10]
459482

460483

461-
def test_complex_cast():
484+
def test_complex_cast(doc):
462485
"""std::complex casts"""
486+
487+
class Complex:
488+
def __complex__(self) -> complex:
489+
return complex(5, 4)
490+
491+
class Float:
492+
def __float__(self) -> float:
493+
return 5.0
494+
495+
class Int:
496+
def __int__(self) -> int:
497+
return 3
498+
499+
class Index:
500+
def __index__(self) -> int:
501+
return 1
502+
463503
assert m.complex_cast(1) == "1.0"
504+
assert m.complex_cast(1.0) == "1.0"
505+
assert m.complex_cast(Complex()) == "(5.0, 4.0)"
464506
assert m.complex_cast(2j) == "(0.0, 2.0)"
465507

508+
convert, noconvert = m.complex_convert, m.complex_noconvert
509+
510+
def requires_conversion(v):
511+
pytest.raises(TypeError, noconvert, v)
512+
513+
def cant_convert(v):
514+
pytest.raises(TypeError, convert, v)
515+
516+
assert (
517+
doc(convert)
518+
== "complex_convert(arg0: typing.SupportsComplex | typing.SupportsFloat | typing.SupportsIndex) -> complex"
519+
)
520+
assert doc(noconvert) == "complex_noconvert(arg0: complex) -> complex"
521+
522+
assert convert(1) == 1.0
523+
assert convert(2.0) == 2.0
524+
assert convert(1 + 5j) == 1.0 + 5.0j
525+
assert convert(Complex()) == 5.0 + 4j
526+
assert convert(Float()) == 5.0
527+
assert isinstance(convert(Float()), complex)
528+
cant_convert(Int())
529+
assert convert(Index()) == 1
530+
assert isinstance(convert(Index()), complex)
531+
532+
requires_conversion(1)
533+
requires_conversion(2.0)
534+
assert noconvert(1 + 5j) == 1.0 + 5.0j
535+
requires_conversion(Complex())
536+
requires_conversion(Float())
537+
requires_conversion(Index())
538+
466539

467540
def test_bool_caster():
468541
"""Test bool caster implicit conversions."""

tests/test_callbacks.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,11 +140,11 @@ def test_cpp_function_roundtrip():
140140
def test_function_signatures(doc):
141141
assert (
142142
doc(m.test_callback3)
143-
== "test_callback3(arg0: collections.abc.Callable[[typing.SupportsInt], int]) -> str"
143+
== "test_callback3(arg0: collections.abc.Callable[[typing.SupportsInt | typing.SupportsIndex], int]) -> str"
144144
)
145145
assert (
146146
doc(m.test_callback4)
147-
== "test_callback4() -> collections.abc.Callable[[typing.SupportsInt], int]"
147+
== "test_callback4() -> collections.abc.Callable[[typing.SupportsInt | typing.SupportsIndex], int]"
148148
)
149149

150150

tests/test_class.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -163,13 +163,13 @@ def test_qualname(doc):
163163
assert (
164164
doc(m.NestBase.Nested.fn)
165165
== """
166-
fn(self: m.class_.NestBase.Nested, arg0: typing.SupportsInt, arg1: m.class_.NestBase, arg2: m.class_.NestBase.Nested) -> None
166+
fn(self: m.class_.NestBase.Nested, arg0: typing.SupportsInt | typing.SupportsIndex, arg1: m.class_.NestBase, arg2: m.class_.NestBase.Nested) -> None
167167
"""
168168
)
169169
assert (
170170
doc(m.NestBase.Nested.fa)
171171
== """
172-
fa(self: m.class_.NestBase.Nested, a: typing.SupportsInt, b: m.class_.NestBase, c: m.class_.NestBase.Nested) -> None
172+
fa(self: m.class_.NestBase.Nested, a: typing.SupportsInt | typing.SupportsIndex, b: m.class_.NestBase, c: m.class_.NestBase.Nested) -> None
173173
"""
174174
)
175175
assert m.NestBase.__module__ == "pybind11_tests.class_"

tests/test_custom_type_casters.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ def test_noconvert_args(msg):
7575
msg(excinfo.value)
7676
== """
7777
ints_preferred(): incompatible function arguments. The following argument types are supported:
78-
1. (i: typing.SupportsInt) -> int
78+
1. (i: typing.SupportsInt | typing.SupportsIndex) -> int
7979
8080
Invoked with: 4.0
8181
"""

tests/test_docstring_options.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,11 @@ def test_docstring_options():
2020

2121
# options.enable_function_signatures()
2222
assert m.test_function3.__doc__.startswith(
23-
"test_function3(a: typing.SupportsInt, b: typing.SupportsInt) -> None"
23+
"test_function3(a: typing.SupportsInt | typing.SupportsIndex, b: typing.SupportsInt | typing.SupportsIndex) -> None"
2424
)
2525

2626
assert m.test_function4.__doc__.startswith(
27-
"test_function4(a: typing.SupportsInt, b: typing.SupportsInt) -> None"
27+
"test_function4(a: typing.SupportsInt | typing.SupportsIndex, b: typing.SupportsInt | typing.SupportsIndex) -> None"
2828
)
2929
assert m.test_function4.__doc__.endswith("A custom docstring\n")
3030

@@ -37,7 +37,7 @@ def test_docstring_options():
3737

3838
# RAII destructor
3939
assert m.test_function7.__doc__.startswith(
40-
"test_function7(a: typing.SupportsInt, b: typing.SupportsInt) -> None"
40+
"test_function7(a: typing.SupportsInt | typing.SupportsIndex, b: typing.SupportsInt | typing.SupportsIndex) -> None"
4141
)
4242
assert m.test_function7.__doc__.endswith("A custom docstring\n")
4343

tests/test_enum.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -328,7 +328,7 @@ def test_generated_dunder_methods_pos_only():
328328
)
329329
assert (
330330
re.match(
331-
r"^__setstate__\(self: [\w\.]+, state: [\w\.]+, /\)",
331+
r"^__setstate__\(self: [\w\.]+, state: [\w\. \|]+, /\)",
332332
enum_type.__setstate__.__doc__,
333333
)
334334
is not None

0 commit comments

Comments
 (0)