diff --git a/docs/changelog.rst b/docs/changelog.rst index 5702a5d6..bde4da7e 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -15,6 +15,14 @@ case, both modules must use the same nanobind ABI version, or they will be isolated from each other. Releases that don't explicitly mention an ABI version below inherit that of the preceding release. +Version TBD (unreleased) +------------------------ + +- The ``std::variant`` type_caster now does two passes when converting from Python. + The first pass is done without implicit conversions. This fixes an issue where + ``std::variant`` might cast a Python object wrapping a ``T`` to a ``U`` if + there is an implicit conversion available from ``T`` to ``U``. + Version 2.2.0 (October 3, 2024) ------------------------------- diff --git a/include/nanobind/stl/variant.h b/include/nanobind/stl/variant.h index d2804255..473ec8fa 100644 --- a/include/nanobind/stl/variant.h +++ b/include/nanobind/stl/variant.h @@ -45,6 +45,11 @@ template struct type_caster> { } bool from_python(handle src, uint8_t flags, cleanup_list *cleanup) noexcept { + if (flags & (uint8_t) cast_flags::convert) { + if ((try_variant(src, flags & ~(uint8_t)cast_flags::convert, cleanup) || ...)){ + return true; + } + } return (try_variant(src, flags, cleanup) || ...); } diff --git a/tests/test_stl.cpp b/tests/test_stl.cpp index 834ccd46..7de8a0b0 100644 --- a/tests/test_stl.cpp +++ b/tests/test_stl.cpp @@ -475,4 +475,35 @@ NB_MODULE(test_stl_ext, m) { m.def("optional_cstr", [](std::optional arg) { return arg.value_or("none"); }, nb::arg().none()); + + + // test73 + struct BasicID1 { + uint64_t id; + BasicID1(uint64_t id) : id(id) {} + }; + + struct BasicID2 { + uint64_t id; + BasicID2(uint64_t id) : id(id) {} + }; + + nb::class_(m, "BasicID1") + .def(nb::init()) + .def("__int__", [](const BasicID1& x) { return x.id; }) + ; + + nb::class_(m, "BasicID2") + .def(nb::init_implicit()); + + using IDVariants = std::variant; + + struct IDHavingEvent { + IDVariants id; + IDHavingEvent() = default; + }; + + nb::class_(m, "IDHavingEvent") + .def(nb::init<>()) + .def_rw("id", &IDHavingEvent::id); } diff --git a/tests/test_stl.py b/tests/test_stl.py index f5fc46f0..771e93b2 100644 --- a/tests/test_stl.py +++ b/tests/test_stl.py @@ -794,3 +794,9 @@ def test71_null_input(): @skip_on_pypy # PyPy fails this test on Windows :-( def test72_wstr(): assert t.pass_wstr('🎈') == '🎈' + +def test73_variant_implicit_conversions(): + event = t.IDHavingEvent() + assert event.id is None + event.id = t.BasicID1(78) + assert type(event.id) is t.BasicID1 diff --git a/tests/test_stl_ext.pyi.ref b/tests/test_stl_ext.pyi.ref index e62b4749..5f74b109 100644 --- a/tests/test_stl_ext.pyi.ref +++ b/tests/test_stl_ext.pyi.ref @@ -4,6 +4,14 @@ import pathlib from typing import overload +class BasicID1: + def __init__(self, arg: int, /) -> None: ... + + def __int__(self) -> int: ... + +class BasicID2: + def __init__(self, arg: int, /) -> None: ... + class ClassWithMovableField: def __init__(self) -> None: ... @@ -38,6 +46,15 @@ class FuncWrapper: alive: int = ... """static read-only property""" +class IDHavingEvent: + def __init__(self) -> None: ... + + @property + def id(self) -> None | BasicID2 | BasicID1: ... + + @id.setter + def id(self, arg: BasicID2 | BasicID1, /) -> None: ... + class Movable: @overload def __init__(self) -> None: ...