@@ -315,6 +315,7 @@ def cant_convert(v):
315315 # Before Python 3.8, `PyLong_AsLong` does not pick up on `obj.__index__`,
316316 # but pybind11 "backports" this behavior.
317317 assert convert (Index ()) == 42
318+ assert isinstance (convert (Index ()), int )
318319 assert noconvert (Index ()) == 42
319320 assert convert (IntAndIndex ()) == 0 # Fishy; `int(DoubleThought)` == 42
320321 assert noconvert (IntAndIndex ()) == 0
@@ -323,6 +324,50 @@ def cant_convert(v):
323324 assert convert (RaisingValueErrorOnIndex ()) == 42
324325 requires_conversion (RaisingValueErrorOnIndex ())
325326
327+ class IndexReturnsFloat :
328+ def __index__ (self ):
329+ return 3.14 # noqa: PLE0305 Wrong: should return int
330+
331+ class IntReturnsFloat :
332+ def __int__ (self ):
333+ return 3.14 # Wrong: should return int
334+
335+ class IndexFloatIntInt :
336+ def __index__ (self ):
337+ return 3.14 # noqa: PLE0305 Wrong: should return int
338+
339+ def __int__ (self ):
340+ return 42 # Correct: returns int
341+
342+ class IndexIntIntFloat :
343+ def __index__ (self ):
344+ return 42 # Correct: returns int
345+
346+ def __int__ (self ):
347+ return 3.14 # Wrong: should return int
348+
349+ class IndexFloatIntFloat :
350+ def __index__ (self ):
351+ return 3.14 # noqa: PLE0305 Wrong: should return int
352+
353+ def __int__ (self ):
354+ return 2.71 # Wrong: should return int
355+
356+ cant_convert (IndexReturnsFloat ())
357+ requires_conversion (IndexReturnsFloat ())
358+
359+ cant_convert (IntReturnsFloat ())
360+ requires_conversion (IntReturnsFloat ())
361+
362+ assert convert (IndexFloatIntInt ()) == 42 # convert: __index__ fails, uses __int__
363+ requires_conversion (IndexFloatIntInt ()) # noconvert: __index__ fails, no fallback
364+
365+ assert convert (IndexIntIntFloat ()) == 42 # convert: __index__ succeeds
366+ assert noconvert (IndexIntIntFloat ()) == 42 # noconvert: __index__ succeeds
367+
368+ cant_convert (IndexFloatIntFloat ()) # convert mode rejects (both fail)
369+ requires_conversion (IndexFloatIntFloat ()) # noconvert mode also rejects
370+
326371
327372def test_float_convert (doc ):
328373 class Int :
@@ -356,7 +401,7 @@ def cant_convert(v):
356401 assert pytest .approx (convert (Index ())) == - 7.0
357402 assert isinstance (convert (Float ()), float )
358403 assert pytest .approx (convert (3 )) == 3.0
359- requires_conversion ( 3 )
404+ assert pytest . approx ( noconvert ( 3 )) == 3.0
360405 cant_convert (Int ())
361406
362407
@@ -505,6 +550,11 @@ def __index__(self) -> int:
505550 assert m .complex_cast (Complex ()) == "(5.0, 4.0)"
506551 assert m .complex_cast (2j ) == "(0.0, 2.0)"
507552
553+ assert m .complex_cast_strict (1 ) == "(1.0, 0.0)"
554+ assert m .complex_cast_strict (3.0 ) == "(3.0, 0.0)"
555+ assert m .complex_cast_strict (complex (5 , 4 )) == "(5.0, 4.0)"
556+ assert m .complex_cast_strict (2j ) == "(0.0, 2.0)"
557+
508558 convert , noconvert = m .complex_convert , m .complex_noconvert
509559
510560 def requires_conversion (v ):
@@ -529,14 +579,127 @@ def cant_convert(v):
529579 assert convert (Index ()) == 1
530580 assert isinstance (convert (Index ()), complex )
531581
532- requires_conversion (1 )
533- requires_conversion (2.0 )
582+ assert noconvert (1 ) == 1.0
583+ assert noconvert (2.0 ) == 2.0
534584 assert noconvert (1 + 5j ) == 1.0 + 5.0j
535585 requires_conversion (Complex ())
536586 requires_conversion (Float ())
537587 requires_conversion (Index ())
538588
539589
590+ def test_complex_index_handling ():
591+ """
592+ Test __index__ handling in complex caster (added with PR #5879).
593+
594+ This test verifies that custom __index__ objects (not PyLong) work correctly
595+ with complex conversion. The behavior should be consistent across CPython,
596+ PyPy, and GraalPy.
597+
598+ - Custom __index__ objects work with convert (non-strict mode)
599+ - Custom __index__ objects do NOT work with noconvert (strict mode)
600+ - Regular int (PyLong) works with both convert and noconvert
601+ """
602+
603+ class CustomIndex :
604+ """Custom class with __index__ but not __int__ or __float__"""
605+
606+ def __index__ (self ) -> int :
607+ return 42
608+
609+ class CustomIndexNegative :
610+ """Custom class with negative __index__"""
611+
612+ def __index__ (self ) -> int :
613+ return - 17
614+
615+ convert , noconvert = m .complex_convert , m .complex_noconvert
616+
617+ # Test that regular int (PyLong) works
618+ assert convert (5 ) == 5.0 + 0j
619+ assert noconvert (5 ) == 5.0 + 0j
620+
621+ # Test that custom __index__ objects work with convert (non-strict mode)
622+ # This exercises the PyPy-specific path in complex.h
623+ assert convert (CustomIndex ()) == 42.0 + 0j
624+ assert convert (CustomIndexNegative ()) == - 17.0 + 0j
625+
626+ # With noconvert (strict mode), custom __index__ objects are NOT accepted
627+ # Strict mode only accepts complex, float, or int (PyLong), not custom __index__ objects
628+ def requires_conversion (v ):
629+ pytest .raises (TypeError , noconvert , v )
630+
631+ requires_conversion (CustomIndex ())
632+ requires_conversion (CustomIndexNegative ())
633+
634+ # Verify the result is actually a complex
635+ result = convert (CustomIndex ())
636+ assert isinstance (result , complex )
637+ assert result .real == 42.0
638+ assert result .imag == 0.0
639+
640+
641+ def test_overload_resolution_float_int ():
642+ """
643+ Test overload resolution behavior when int can match float (added with PR #5879).
644+
645+ This test documents the breaking change in PR #5879: when a float overload is
646+ registered before an int overload, passing a Python int will now match the float
647+ overload (because int can be converted to float in strict mode per PEP 484).
648+
649+ Before PR #5879: int(42) would match int overload (if both existed)
650+ After PR #5879: int(42) matches float overload (if registered first)
651+
652+ This is a breaking change because existing code that relied on int matching
653+ int overloads may now match float overloads instead.
654+ """
655+ # Test 1: float overload registered first, int second
656+ # When passing int(42), pybind11 tries overloads in order:
657+ # 1. float overload - can int(42) be converted? Yes (with PR #5879 changes)
658+ # 2. Match! Use float overload (int overload never checked)
659+ result = m .overload_resolution_test (42 )
660+ assert result == "float: 42.000000" , (
661+ f"Expected int(42) to match float overload, got: { result } . "
662+ "This documents the breaking change: int now matches float overloads."
663+ )
664+ assert m .overload_resolution_test (42.0 ) == "float: 42.000000"
665+
666+ # Test 2: With noconvert (strict mode) - this is the KEY breaking change
667+ # Before PR #5879: int(42) would NOT match float overload with noconvert, would match int overload
668+ # After PR #5879: int(42) DOES match float overload with noconvert (because int->float is now allowed)
669+ result_strict = m .overload_resolution_strict (42 )
670+ assert result_strict == "float_strict: 42.000000" , (
671+ f"Expected int(42) to match float overload with noconvert, got: { result_strict } . "
672+ "This is the key breaking change: int now matches float even in strict mode."
673+ )
674+ assert m .overload_resolution_strict (42.0 ) == "float_strict: 42.000000"
675+
676+ # Test 3: complex overload registered first, then float, then int
677+ # When passing int(5), pybind11 tries overloads in order:
678+ # 1. complex overload - can int(5) be converted? Yes (with PR #5879 changes)
679+ # 2. Match! Use complex overload
680+ assert m .overload_resolution_complex (5 ) == "complex: (5.000000, 0.000000)"
681+ assert m .overload_resolution_complex (5.0 ) == "complex: (5.000000, 0.000000)"
682+ assert (
683+ m .overload_resolution_complex (complex (3 , 4 )) == "complex: (3.000000, 4.000000)"
684+ )
685+
686+ # Verify that the overloads are registered in the expected order
687+ # The docstring should show float overload before int overload
688+ doc = m .overload_resolution_test .__doc__
689+ assert doc is not None
690+ # Check that float overload appears before int overload in docstring
691+ # The docstring uses "typing.SupportsFloat" and "typing.SupportsInt"
692+ float_pos = doc .find ("SupportsFloat" )
693+ int_pos = doc .find ("SupportsInt" )
694+ assert float_pos != - 1 , f"Could not find 'SupportsFloat' in docstring: { doc } "
695+ assert int_pos != - 1 , f"Could not find 'SupportsInt' in docstring: { doc } "
696+ assert float_pos < int_pos , (
697+ f"Float overload should appear before int overload in docstring. "
698+ f"Found 'SupportsFloat' at { float_pos } , 'SupportsInt' at { int_pos } . "
699+ f"Docstring: { doc } "
700+ )
701+
702+
540703def test_bool_caster ():
541704 """Test bool caster implicit conversions."""
542705 convert , noconvert = m .bool_passthrough , m .bool_passthrough_noconvert
0 commit comments