* 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>
784 lines
26 KiB
Python
784 lines
26 KiB
Python
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
|