Files
pybind11/tests/test_builtin_casters.py
Henry SchreinerandRalf W. Grosse-Kunstleve 63d627c574 feat!: drop support for Python 3.8, MSVC 2017 (#6110)
* feat!: drop support for Python 3.8

The minimum supported version is now Python 3.9. pybind11 v3.0 was the last
release that supports Python 3.8. The deprecation note said that support goes
away in 3.1.

Remove the code paths that this makes dead:

- the `_PyObject_Vectorcall` fallback in `cast.h`
- the `frame->f_code` and `frame->f_back` fallbacks in `pytypes.h`
- the `PyFrame_FastToLocals` path in `get_type_override`
- the conditional `Py_VISIT(Py_TYPE(self))` in `tp_traverse`
- `PYBIND11_PYCONFIG_SUPPORT_PY_VERSION_HEX`, the pre-PyConfig interpreter
  init, and the `widen_chars` helpers in `embed.h`

Assisted-by: ClaudeCode:claude-opus-5

* ci(appveyor): use Python 3.9

The AppVeyor job set `PYTHON: 38`, which makes the path `C:\Python38`.
The image gives Python 3.9.13 as `C:\Python39`.

Assisted-by: ClaudeCode:claude-opus-5

* feat!: require MSVC 2019 or newer

Python 3.9 is the new minimum, so MSVC 2017 is no longer needed. Raise the
compile-time floor to _MSC_VER 1920 and remove the workarounds that only
applied below it: std::launder, fold expressions, weak_from_this, aligned
new/delete, the C4100 warning helper, and the func_handle syntax error.

AppVeyor now builds with Visual Studio 2019.

Assisted-by: ClaudeCode:claude-opus-5

* fix(appveyor): build against the Python that has the test packages

CMake 4 has no FindPythonLibs, so pybind11 uses FindPython. FindPython reads
the registry before PATH and selected `C:\Python314-x64`, but the test packages
go into the Python on PATH. Pass `Python_ROOT_DIR` to name the correct one.

Also set `CMAKE_ARCH`. It was never set, so the architecture came from the
generator. Visual Studio 2017 defaults to Win32, but Visual Studio 2019
defaults to x64, which made this x86 job build 64-bit code.

Assisted-by: ClaudeCode:claude-opus-5

* fix(appveyor): give the linker Python's libs directory

The build compiled but failed to link with LNK1104 on a bare `python39.lib`.
That name comes from the `#pragma comment(lib, ...)` in pyconfig.h, so the
linker needs the directory. CMake 4 has no FindPythonLibs, and FindPython does
not add it for this Debug x86 build. Put it on LIB instead.

The directory listing is temporary, to confirm the library is present.

Assisted-by: ClaudeCode:claude-opus-5

* fix(appveyor): link the release Python library in Debug

The image ships python39_d.lib next to python39.lib, so FindPython picks the
debug import library for a Debug build. pybind11 undefines _DEBUG around
Python.h, so pyconfig.h asks for python39.lib instead and the link failed with
LNK1104. Name the release library for the debug slot.

Setting LIB does not work, because MSBuild replaces it from the toolset, and it
would link both import libraries.

Assisted-by: ClaudeCode:claude-opus-5

* chore(appveyor): print link settings to debug LNK1104

Revert the two attempted fixes. Neither changed the failure: setting LIB does
not survive MSBuild, and naming the release library for Python_LIBRARY_DEBUG
had no effect.

Print the Python cache entries and the link settings of a generated project
file instead, to see what the linker really gets. Temporary.

Assisted-by: ClaudeCode:claude-opus-5

* fix(appveyor): take the release Python library in Debug

The generated project file linked C:\Python39\libs\python39_d.lib in the Debug
configuration, because the image ships debug binaries next to the release ones.
pybind11 undefines _DEBUG around Python.h, so pyconfig.h asks for python39.lib
in a #pragma comment(lib), which nothing on the link line satisfies and no
library directory holds. Map Debug to the release artifacts.

Assisted-by: ClaudeCode:claude-opus-5

* chore(appveyor): drop the temporary link diagnostic

Assisted-by: ClaudeCode:claude-opus-5

* fix(tests): guard the unraisable warning filter for pytest < 6

The distro pytest in the Clang and GCC Docker jobs has no
PytestUnraisableExceptionWarning, so an unconditional filterwarnings
mark makes pytest fail with an INTERNALERROR after the tests pass.

Assisted-by: ClaudeCode:claude-opus-5

Claude-Session: https://claude.ai/code/session_01Aimf6HuSz1vLRwBnbxmCTc

* docs: address review items on version hints, embed docs, and a PyPy xfail

Extend Python_ADDITIONAL_VERSIONS through 3.15, describe the PyConfig
behavior of initialize_interpreter, and drop the stale Python 3.8 wording
from the PyPy xfail reason.

Assisted-by: ClaudeCode:claude-opus-5

* Update README.rst

Co-authored-by: Ralf W. Grosse-Kunstleve <rwgkio@gmail.com>

---------

Co-authored-by: Ralf W. Grosse-Kunstleve <rwgkio@gmail.com>
2026-07-31 22:04:32 -07:00

784 lines
26 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
import sys
import pytest
import env
from pybind11_tests import IncType, UserType
from pybind11_tests import builtin_casters as m
def test_simple_string():
assert m.string_roundtrip("const char *") == "const char *"
def test_unicode_conversion():
"""Tests unicode conversion and error reporting."""
assert m.good_utf8_string() == "Say utf8‽ 🎂 𝐀"
assert m.good_utf16_string() == "b‽🎂𝐀z"
assert m.good_utf32_string() == "a𝐀🎂‽z"
assert m.good_wchar_string() == "a⸘𝐀z"
if hasattr(m, "has_u8string"):
assert m.good_utf8_u8string() == "Say utf8‽ 🎂 𝐀"
with pytest.raises(UnicodeDecodeError):
m.bad_utf8_string()
with pytest.raises(UnicodeDecodeError):
m.bad_utf16_string()
# These are provided only if they actually fail (they don't when 32-bit)
if hasattr(m, "bad_utf32_string"):
with pytest.raises(UnicodeDecodeError):
m.bad_utf32_string()
if hasattr(m, "bad_wchar_string"):
with pytest.raises(UnicodeDecodeError):
m.bad_wchar_string()
if hasattr(m, "has_u8string"):
with pytest.raises(UnicodeDecodeError):
m.bad_utf8_u8string()
assert m.u8_Z() == "Z"
assert m.u8_eacute() == "é"
assert m.u16_ibang() == "‽"
assert m.u32_mathbfA() == "𝐀"
assert m.wchar_heart() == "♥"
if hasattr(m, "has_u8string"):
assert m.u8_char8_Z() == "Z"
def test_single_char_arguments():
"""Tests failures for passing invalid inputs to char-accepting functions"""
def toobig_message(r):
return f"Character code point not in range({r:#x})"
toolong_message = "Expected a character, but multi-character string found"
assert m.ord_char("a") == 0x61 # simple ASCII
assert m.ord_char_lv("b") == 0x62
assert (
m.ord_char("é") == 0xE9
) # requires 2 bytes in utf-8, but can be stuffed in a char
with pytest.raises(ValueError) as excinfo:
assert m.ord_char("Ā") == 0x100 # requires 2 bytes, doesn't fit in a char
assert str(excinfo.value) == toobig_message(0x100)
with pytest.raises(ValueError) as excinfo:
assert m.ord_char("ab")
assert str(excinfo.value) == toolong_message
assert m.ord_char16("a") == 0x61
assert m.ord_char16("é") == 0xE9
assert m.ord_char16_lv("ê") == 0xEA
assert m.ord_char16("Ā") == 0x100
assert m.ord_char16("‽") == 0x203D
assert m.ord_char16("♥") == 0x2665
assert m.ord_char16_lv("♡") == 0x2661
with pytest.raises(ValueError) as excinfo:
assert m.ord_char16("🎂") == 0x1F382 # requires surrogate pair
assert str(excinfo.value) == toobig_message(0x10000)
with pytest.raises(ValueError) as excinfo:
assert m.ord_char16("aa")
assert str(excinfo.value) == toolong_message
assert m.ord_char32("a") == 0x61
assert m.ord_char32("é") == 0xE9
assert m.ord_char32("Ā") == 0x100
assert m.ord_char32("‽") == 0x203D
assert m.ord_char32("♥") == 0x2665
assert m.ord_char32("🎂") == 0x1F382
with pytest.raises(ValueError) as excinfo:
assert m.ord_char32("aa")
assert str(excinfo.value) == toolong_message
assert m.ord_wchar("a") == 0x61
assert m.ord_wchar("é") == 0xE9
assert m.ord_wchar("Ā") == 0x100
assert m.ord_wchar("‽") == 0x203D
assert m.ord_wchar("♥") == 0x2665
if m.wchar_size == 2:
with pytest.raises(ValueError) as excinfo:
assert m.ord_wchar("🎂") == 0x1F382 # requires surrogate pair
assert str(excinfo.value) == toobig_message(0x10000)
else:
assert m.ord_wchar("🎂") == 0x1F382
with pytest.raises(ValueError) as excinfo:
assert m.ord_wchar("aa")
assert str(excinfo.value) == toolong_message
if hasattr(m, "has_u8string"):
assert m.ord_char8("a") == 0x61 # simple ASCII
assert m.ord_char8_lv("b") == 0x62
assert (
m.ord_char8("é") == 0xE9
) # requires 2 bytes in utf-8, but can be stuffed in a char
with pytest.raises(ValueError) as excinfo:
assert m.ord_char8("Ā") == 0x100 # requires 2 bytes, doesn't fit in a char
assert str(excinfo.value) == toobig_message(0x100)
with pytest.raises(ValueError) as excinfo:
assert m.ord_char8("ab")
assert str(excinfo.value) == toolong_message
def test_bytes_to_string():
"""Tests the ability to pass bytes to C++ string-accepting functions. Note that this is
one-way: the only way to return bytes to Python is via the pybind11::bytes class."""
# Issue #816
assert m.strlen(b"hi") == 2
assert m.string_length(b"world") == 5
assert m.string_length(b"a\x00b") == 3
assert m.strlen(b"a\x00b") == 1 # C-string limitation
# passing in a utf8 encoded string should work
assert m.string_length("💩".encode()) == 4
def test_bytearray_to_string():
"""Tests the ability to pass bytearray to C++ string-accepting functions"""
assert m.string_length(bytearray(b"Hi")) == 2
assert m.strlen(bytearray(b"bytearray")) == 9
assert m.string_length(bytearray()) == 0
assert m.string_length(bytearray("🦜", "utf-8", "strict")) == 4
assert m.string_length(bytearray(b"\x80")) == 1
@pytest.mark.skipif(not hasattr(m, "has_string_view"), reason="no <string_view>")
def test_string_view(capture):
"""Tests support for C++17 string_view arguments and return values"""
assert m.string_view_chars("Hi") == [72, 105]
assert m.string_view_chars("Hi 🎂") == [72, 105, 32, 0xF0, 0x9F, 0x8E, 0x82]
assert m.string_view16_chars("Hi 🎂") == [72, 105, 32, 0xD83C, 0xDF82]
assert m.string_view32_chars("Hi 🎂") == [72, 105, 32, 127874]
if hasattr(m, "has_u8string"):
assert m.string_view8_chars("Hi") == [72, 105]
assert m.string_view8_chars("Hi 🎂") == [72, 105, 32, 0xF0, 0x9F, 0x8E, 0x82]
assert m.string_view_return() == "utf8 secret 🎂"
assert m.string_view16_return() == "utf16 secret 🎂"
assert m.string_view32_return() == "utf32 secret 🎂"
if hasattr(m, "has_u8string"):
assert m.string_view8_return() == "utf8 secret 🎂"
with capture:
m.string_view_print("Hi")
m.string_view_print("utf8 🎂")
m.string_view16_print("utf16 🎂")
m.string_view32_print("utf32 🎂")
assert (
capture
== """
Hi 2
utf8 🎂 9
utf16 🎂 8
utf32 🎂 7
"""
)
if hasattr(m, "has_u8string"):
with capture:
m.string_view8_print("Hi")
m.string_view8_print("utf8 🎂")
assert (
capture
== """
Hi 2
utf8 🎂 9
"""
)
with capture:
m.string_view_print("Hi, ascii")
m.string_view_print("Hi, utf8 🎂")
m.string_view16_print("Hi, utf16 🎂")
m.string_view32_print("Hi, utf32 🎂")
assert (
capture
== """
Hi, ascii 9
Hi, utf8 🎂 13
Hi, utf16 🎂 12
Hi, utf32 🎂 11
"""
)
if hasattr(m, "has_u8string"):
with capture:
m.string_view8_print("Hi, ascii")
m.string_view8_print("Hi, utf8 🎂")
assert (
capture
== """
Hi, ascii 9
Hi, utf8 🎂 13
"""
)
assert m.string_view_bytes() == b"abc \x80\x80 def"
assert m.string_view_str() == "abc ‽ def"
assert m.string_view_from_bytes("abc ‽ def".encode()) == "abc ‽ def"
if hasattr(m, "has_u8string"):
assert m.string_view8_str() == "abc ‽ def"
assert m.string_view_memoryview() == "Have some 🎂".encode()
assert m.bytes_from_type_with_both_operator_string_and_string_view() == b"success"
assert m.str_from_type_with_both_operator_string_and_string_view() == "success"
def test_integer_casting():
"""Issue #929 - out-of-range integer values shouldn't be accepted"""
assert m.i32_str(-1) == "-1"
assert m.i64_str(-1) == "-1"
assert m.i32_str(2000000000) == "2000000000"
assert m.u32_str(2000000000) == "2000000000"
assert m.i64_str(-999999999999) == "-999999999999"
assert m.u64_str(999999999999) == "999999999999"
with pytest.raises(TypeError) as excinfo:
m.u32_str(-1)
assert "incompatible function arguments" in str(excinfo.value)
with pytest.raises(TypeError) as excinfo:
m.u64_str(-1)
assert "incompatible function arguments" in str(excinfo.value)
with pytest.raises(TypeError) as excinfo:
m.i32_str(-3000000000)
assert "incompatible function arguments" in str(excinfo.value)
with pytest.raises(TypeError) as excinfo:
m.i32_str(3000000000)
assert "incompatible function arguments" in str(excinfo.value)
def test_int_convert(doc):
class Int:
def __int__(self):
return 42
class NotInt:
pass
class Float:
def __float__(self):
return 41.99999
class Index:
def __index__(self):
return 42
class IntAndIndex:
def __int__(self):
return 42
def __index__(self):
return 0
class RaisingTypeErrorOnIndex:
def __index__(self):
raise TypeError
def __int__(self):
return 42
class RaisingValueErrorOnIndex:
def __index__(self):
raise ValueError
def __int__(self):
return 42
convert, noconvert = m.int_passthrough, m.int_passthrough_noconvert
assert (
doc(convert)
== "int_passthrough(arg0: typing.SupportsInt | typing.SupportsIndex) -> int"
)
assert doc(noconvert) == "int_passthrough_noconvert(arg0: int) -> int"
def requires_conversion(v):
pytest.raises(TypeError, noconvert, v)
def cant_convert(v):
pytest.raises(TypeError, convert, v)
assert convert(7) == 7
assert noconvert(7) == 7
cant_convert(3.14159)
# TODO: Avoid DeprecationWarning in `PyLong_AsLong` (and similar)
if sys.version_info < (3, 10) and env.CPYTHON:
with pytest.deprecated_call():
assert convert(Int()) == 42
else:
assert convert(Int()) == 42
requires_conversion(Int())
cant_convert(NotInt())
cant_convert(Float())
assert convert(Index()) == 42
assert isinstance(convert(Index()), int)
assert noconvert(Index()) == 42
assert convert(IntAndIndex()) == 0 # Fishy; `int(DoubleThought)` == 42
assert noconvert(IntAndIndex()) == 0
assert convert(RaisingTypeErrorOnIndex()) == 42
requires_conversion(RaisingTypeErrorOnIndex())
assert convert(RaisingValueErrorOnIndex()) == 42
requires_conversion(RaisingValueErrorOnIndex())
class IndexReturnsFloat:
def __index__(self):
return 3.14 # noqa: PLE0305 Wrong: should return int
class IntReturnsFloat:
def __int__(self):
return 3.14 # Wrong: should return int
class IndexFloatIntInt:
def __index__(self):
return 3.14 # noqa: PLE0305 Wrong: should return int
def __int__(self):
return 42 # Correct: returns int
class IndexIntIntFloat:
def __index__(self):
return 42 # Correct: returns int
def __int__(self):
return 3.14 # Wrong: should return int
class IndexFloatIntFloat:
def __index__(self):
return 3.14 # noqa: PLE0305 Wrong: should return int
def __int__(self):
return 2.71 # Wrong: should return int
cant_convert(IndexReturnsFloat())
requires_conversion(IndexReturnsFloat())
cant_convert(IntReturnsFloat())
requires_conversion(IntReturnsFloat())
assert convert(IndexFloatIntInt()) == 42 # convert: __index__ fails, uses __int__
requires_conversion(IndexFloatIntInt()) # noconvert: __index__ fails, no fallback
assert convert(IndexIntIntFloat()) == 42 # convert: __index__ succeeds
assert noconvert(IndexIntIntFloat()) == 42 # noconvert: __index__ succeeds
cant_convert(IndexFloatIntFloat()) # convert mode rejects (both fail)
requires_conversion(IndexFloatIntFloat()) # noconvert mode also rejects
def test_float_convert(doc):
class Int:
def __int__(self):
return -5
class Index:
def __index__(self) -> int:
return -7
class Float:
def __float__(self):
return 41.45
convert, noconvert = m.float_passthrough, m.float_passthrough_noconvert
assert (
doc(convert)
== "float_passthrough(arg0: typing.SupportsFloat | typing.SupportsIndex) -> float"
)
assert doc(noconvert) == "float_passthrough_noconvert(arg0: float) -> float"
def requires_conversion(v):
pytest.raises(TypeError, noconvert, v)
def cant_convert(v):
pytest.raises(TypeError, convert, v)
requires_conversion(Float())
requires_conversion(Index())
assert pytest.approx(convert(Float())) == 41.45
assert pytest.approx(convert(Index())) == -7.0
assert isinstance(convert(Float()), float)
assert pytest.approx(convert(3)) == 3.0
assert pytest.approx(noconvert(3)) == 3.0
cant_convert(Int())
def test_numpy_int_convert():
np = pytest.importorskip("numpy")
convert, noconvert = m.int_passthrough, m.int_passthrough_noconvert
def require_implicit(v):
pytest.raises(TypeError, noconvert, v)
# `np.intc` is an alias that corresponds to a C++ `int`
assert convert(np.intc(42)) == 42
assert noconvert(np.intc(42)) == 42
# The implicit conversion from np.float32 is undesirable but currently accepted.
# TODO: Avoid DeprecationWarning in `PyLong_AsLong` (and similar)
# https://github.com/pybind/pybind11/issues/3408
if sys.version_info < (3, 10) and env.CPYTHON:
with pytest.deprecated_call():
assert convert(np.float32(3.14159)) == 3
else:
assert convert(np.float32(3.14159)) == 3
require_implicit(np.float32(3.14159))
def test_tuple(doc):
"""std::pair <-> tuple & std::tuple <-> tuple"""
assert m.pair_passthrough((True, "test")) == ("test", True)
assert m.tuple_passthrough((True, "test", 5)) == (5, "test", True)
# Any sequence can be cast to a std::pair or std::tuple
assert m.pair_passthrough([True, "test"]) == ("test", True)
assert m.tuple_passthrough([True, "test", 5]) == (5, "test", True)
assert m.empty_tuple() == ()
assert (
doc(m.pair_passthrough)
== """
pair_passthrough(arg0: tuple[bool, str]) -> tuple[str, bool]
Return a pair in reversed order
"""
)
assert (
doc(m.tuple_passthrough)
== """
tuple_passthrough(arg0: tuple[bool, str, typing.SupportsInt | typing.SupportsIndex]) -> tuple[int, str, bool]
Return a triple in reversed order
"""
)
assert doc(m.empty_tuple) == """empty_tuple() -> tuple[()]"""
assert m.rvalue_pair() == ("rvalue", "rvalue")
assert m.lvalue_pair() == ("lvalue", "lvalue")
assert m.rvalue_tuple() == ("rvalue", "rvalue", "rvalue")
assert m.lvalue_tuple() == ("lvalue", "lvalue", "lvalue")
assert m.rvalue_nested() == ("rvalue", ("rvalue", ("rvalue", "rvalue")))
assert m.lvalue_nested() == ("lvalue", ("lvalue", ("lvalue", "lvalue")))
assert m.int_string_pair() == (2, "items")
def test_builtins_cast_return_none():
"""Casters produced with PYBIND11_TYPE_CASTER() should convert nullptr to None"""
assert m.return_none_string() is None
assert m.return_none_char() is None
assert m.return_none_bool() is None
assert m.return_none_int() is None
assert m.return_none_float() is None
assert m.return_none_pair() is None
def test_none_deferred():
"""None passed as various argument types should defer to other overloads"""
assert not m.defer_none_cstring("abc")
assert m.defer_none_cstring(None)
assert not m.defer_none_custom(UserType())
assert m.defer_none_custom(None)
assert m.nodefer_none_void(None)
def test_void_caster():
assert m.load_nullptr_t(None) is None
assert m.cast_nullptr_t() is None
def test_reference_wrapper():
"""std::reference_wrapper for builtin and user types"""
assert m.refwrap_builtin(42) == 420
assert m.refwrap_usertype(UserType(42)) == 42
assert m.refwrap_usertype_const(UserType(42)) == 42
with pytest.raises(TypeError) as excinfo:
m.refwrap_builtin(None)
assert "incompatible function arguments" in str(excinfo.value)
with pytest.raises(TypeError) as excinfo:
m.refwrap_usertype(None)
assert "incompatible function arguments" in str(excinfo.value)
assert m.refwrap_lvalue().value == 1
assert m.refwrap_lvalue_const().value == 1
a1 = m.refwrap_list(copy=True)
a2 = m.refwrap_list(copy=True)
assert [x.value for x in a1] == [2, 3]
assert [x.value for x in a2] == [2, 3]
assert a1[0] is not a2[0]
assert a1[1] is not a2[1]
b1 = m.refwrap_list(copy=False)
b2 = m.refwrap_list(copy=False)
assert [x.value for x in b1] == [1, 2]
assert [x.value for x in b2] == [1, 2]
assert b1[0] is b2[0]
assert b1[1] is b2[1]
assert m.refwrap_iiw(IncType(5)) == 5
assert m.refwrap_call_iiw(IncType(10), m.refwrap_iiw) == [10, 10, 10, 10]
def test_complex_cast(doc):
"""std::complex casts"""
class Complex:
def __complex__(self) -> complex:
return complex(5, 4)
class Float:
def __float__(self) -> float:
return 5.0
class Int:
def __int__(self) -> int:
return 3
class Index:
def __index__(self) -> int:
return 1
assert m.complex_cast(1) == "1.0"
assert m.complex_cast(1.0) == "1.0"
assert m.complex_cast(Complex()) == "(5.0, 4.0)"
assert m.complex_cast(2j) == "(0.0, 2.0)"
assert m.complex_cast_strict(1) == "(1.0, 0.0)"
assert m.complex_cast_strict(3.0) == "(3.0, 0.0)"
assert m.complex_cast_strict(complex(5, 4)) == "(5.0, 4.0)"
assert m.complex_cast_strict(2j) == "(0.0, 2.0)"
convert, noconvert = m.complex_convert, m.complex_noconvert
def requires_conversion(v):
pytest.raises(TypeError, noconvert, v)
def cant_convert(v):
pytest.raises(TypeError, convert, v)
assert (
doc(convert)
== "complex_convert(arg0: complex | typing.SupportsComplex | typing.SupportsFloat | typing.SupportsIndex) -> complex"
)
assert doc(noconvert) == "complex_noconvert(arg0: complex) -> complex"
assert convert(1) == 1.0
assert convert(2.0) == 2.0
assert convert(1 + 5j) == 1.0 + 5.0j
assert convert(Complex()) == 5.0 + 4j
assert convert(Float()) == 5.0
assert isinstance(convert(Float()), complex)
cant_convert(Int())
assert convert(Index()) == 1
assert isinstance(convert(Index()), complex)
assert noconvert(1) == 1.0
assert noconvert(2.0) == 2.0
assert noconvert(1 + 5j) == 1.0 + 5.0j
requires_conversion(Complex())
requires_conversion(Float())
requires_conversion(Index())
def test_complex_index_handling():
"""
Test __index__ handling in complex caster (added with PR #5879).
This test verifies that custom __index__ objects (not PyLong) work correctly
with complex conversion. The behavior should be consistent across CPython,
PyPy, and GraalPy.
- Custom __index__ objects work with convert (non-strict mode)
- Custom __index__ objects do NOT work with noconvert (strict mode)
- Regular int (PyLong) works with both convert and noconvert
"""
class CustomIndex:
"""Custom class with __index__ but not __int__ or __float__"""
def __index__(self) -> int:
return 42
class CustomIndexNegative:
"""Custom class with negative __index__"""
def __index__(self) -> int:
return -17
convert, noconvert = m.complex_convert, m.complex_noconvert
# Test that regular int (PyLong) works
assert convert(5) == 5.0 + 0j
assert noconvert(5) == 5.0 + 0j
# Test that custom __index__ objects work with convert (non-strict mode)
# This exercises the PyPy-specific path in complex.h
assert convert(CustomIndex()) == 42.0 + 0j
assert convert(CustomIndexNegative()) == -17.0 + 0j
# With noconvert (strict mode), custom __index__ objects are NOT accepted
# Strict mode only accepts complex, float, or int (PyLong), not custom __index__ objects
def requires_conversion(v):
pytest.raises(TypeError, noconvert, v)
requires_conversion(CustomIndex())
requires_conversion(CustomIndexNegative())
# Verify the result is actually a complex
result = convert(CustomIndex())
assert isinstance(result, complex)
assert result.real == 42.0
assert result.imag == 0.0
def test_overload_resolution_float_int():
"""
Test overload resolution behavior when int can match float (added with PR #5879).
This test documents the breaking change in PR #5879: when a float overload is
registered before an int overload, passing a Python int will now match the float
overload (because int can be converted to float in strict mode per PEP 484).
Before PR #5879: int(42) would match int overload (if both existed)
After PR #5879: int(42) matches float overload (if registered first)
This is a breaking change because existing code that relied on int matching
int overloads may now match float overloads instead.
"""
# Test 1: float overload registered first, int second
# When passing int(42), pybind11 tries overloads in order:
# 1. float overload - can int(42) be converted? Yes (with PR #5879 changes)
# 2. Match! Use float overload (int overload never checked)
result = m.overload_resolution_test(42)
assert result == "float: 42.000000", (
f"Expected int(42) to match float overload, got: {result}. "
"This documents the breaking change: int now matches float overloads."
)
assert m.overload_resolution_test(42.0) == "float: 42.000000"
# Test 2: With noconvert (strict mode) - this is the KEY breaking change
# Before PR #5879: int(42) would NOT match float overload with noconvert, would match int overload
# After PR #5879: int(42) DOES match float overload with noconvert (because int->float is now allowed)
result_strict = m.overload_resolution_strict(42)
assert result_strict == "float_strict: 42.000000", (
f"Expected int(42) to match float overload with noconvert, got: {result_strict}. "
"This is the key breaking change: int now matches float even in strict mode."
)
assert m.overload_resolution_strict(42.0) == "float_strict: 42.000000"
# Test 3: complex overload registered first, then float, then int
# When passing int(5), pybind11 tries overloads in order:
# 1. complex overload - can int(5) be converted? Yes (with PR #5879 changes)
# 2. Match! Use complex overload
assert m.overload_resolution_complex(5) == "complex: (5.000000, 0.000000)"
assert m.overload_resolution_complex(5.0) == "complex: (5.000000, 0.000000)"
assert (
m.overload_resolution_complex(complex(3, 4)) == "complex: (3.000000, 4.000000)"
)
# Verify that the overloads are registered in the expected order
# The docstring should show float overload before int overload
doc = m.overload_resolution_test.__doc__
assert doc is not None
# Check that float overload appears before int overload in docstring
# The docstring uses "typing.SupportsFloat" and "typing.SupportsInt"
float_pos = doc.find("SupportsFloat")
int_pos = doc.find("SupportsInt")
assert float_pos != -1, f"Could not find 'SupportsFloat' in docstring: {doc}"
assert int_pos != -1, f"Could not find 'SupportsInt' in docstring: {doc}"
assert float_pos < int_pos, (
f"Float overload should appear before int overload in docstring. "
f"Found 'SupportsFloat' at {float_pos}, 'SupportsInt' at {int_pos}. "
f"Docstring: {doc}"
)
def test_bool_caster():
"""Test bool caster implicit conversions."""
convert, noconvert = m.bool_passthrough, m.bool_passthrough_noconvert
def require_implicit(v):
pytest.raises(TypeError, noconvert, v)
def cant_convert(v):
pytest.raises(TypeError, convert, v)
# straight up bool
assert convert(True) is True
assert convert(False) is False
assert noconvert(True) is True
assert noconvert(False) is False
# None requires implicit conversion
require_implicit(None)
assert convert(None) is False
class A:
def __init__(self, x):
self.x = x
def __nonzero__(self):
return self.x
def __bool__(self):
return self.x
class B:
pass
# Arbitrary objects are not accepted
cant_convert(object())
cant_convert(B())
# Objects with __nonzero__ / __bool__ defined can be converted
require_implicit(A(True))
assert convert(A(True)) is True
assert convert(A(False)) is False
def test_numpy_bool():
np = pytest.importorskip("numpy")
convert, noconvert = m.bool_passthrough, m.bool_passthrough_noconvert
def cant_convert(v):
pytest.raises(TypeError, convert, v)
# np.bool_ is not considered implicit
assert convert(np.bool_(True)) is True
assert convert(np.bool_(False)) is False
assert noconvert(np.bool_(True)) is True
assert noconvert(np.bool_(False)) is False
cant_convert(np.zeros(2, dtype="int"))
def test_int_long():
assert isinstance(m.int_cast(), int)
assert isinstance(m.long_cast(), int)
assert isinstance(m.longlong_cast(), int)
def test_void_caster_2():
assert m.test_void_caster()
def test_const_ref_caster():
"""Verifies that const-ref is propagated through type_caster cast_op.
The returned ConstRefCasted type is a minimal type that is constructed to
reference the casting mode used.
"""
x = False
assert m.takes(x) == 1
assert m.takes_move(x) == 1
assert m.takes_ptr(x) == 3
assert m.takes_ref(x) == 2
assert m.takes_ref_wrap(x) == 2
assert m.takes_const_ptr(x) == 5
assert m.takes_const_ref(x) == 4
assert m.takes_const_ref_wrap(x) == 4