mirror of
https://github.com/pybind/pybind11.git
synced 2026-03-14 20:27:47 +00:00
Enable Conversions Between Native Python Enum Types and C++ Enums (#5555)
* Apply smart_holder-branch-based PR #5280 on top of master. * Add pytest.skip("GraalPy does not raise UnicodeDecodeError") * Add `parent_scope` as first argument to `py::native_enum` ctor. * Replace `operator+=` API with `.finalize()` API. The error messages still need cleanup. * Resolve clang-tidy performance-unnecessary-value-param errors * Rename (effectively) native_enum_add_to_parent() -> finalize() * Update error message: pybind11::native_enum<...>("Fake", ...): MISSING .finalize() * Pass py::module_ by reference to resolve clang-tidy errors (this is entirely inconsequential otherwise for all practical purposes). * test_native_enum_correct_use_failure -> test_native_enum_missing_finalize_failure * Add test_native_enum_double_finalize(), test_native_enum_value_after_finalize() * Clean up public/protected API. * [ci skip] Update the Enumerations section in classes.rst * Rename `py::native_enum_kind` → `py::enum_kind` as suggested by gh-henryiii: https://github.com/pybind/pybind11/pull/5555#issuecomment-2711672335 * Experiment: StrEnum enum.StrEnum does not map to C++ enum: * https://chatgpt.com/share/67d5e965-ccb0-8008-95b7-0df2502309b3 ``` ============================= test session starts ============================== platform linux -- Python 3.12.3, pytest-8.3.3, pluggy-1.5.0 C++ Info: 13.3.0 C++20 __pybind11_internals_v10000000_system_libstdcpp_gxx_abi_1xxx_use_cxx11_abi_1__ PYBIND11_SIMPLE_GIL_MANAGEMENT=False PYBIND11_NUMPY_1_ONLY=False configfile: pytest.ini plugins: parallel-0.1.1, xdist-3.6.1 collected 40 items / 39 deselected / 1 selected test_native_enum.py F [100%] =================================== FAILURES =================================== ________________________ test_native_enum_StrEnum_greek ________________________ def test_native_enum_StrEnum_greek(): assert not hasattr(m, "greek") > m.native_enum_StrEnum_greek(m) test_native_enum.py:150: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ /usr/lib/python3.12/enum.py:764: in __call__ return cls._create_( boundary = None cls = <enum 'StrEnum'> module = None names = [('Alpha', 10), ('Omega', 20)] qualname = None start = 1 type = None value = 'greek' values = () /usr/lib/python3.12/enum.py:917: in _create_ return metacls.__new__(metacls, class_name, bases, classdict, boundary=boundary) _ = <class 'str'> bases = (<enum 'StrEnum'>,) boundary = None class_name = 'greek' classdict = {'_generate_next_value_': <function StrEnum._generate_next_value_ at 0x701ec1711e40>, 'Alpha': 10, 'Omega': 20, '__module__': 'test_native_enum'} cls = <enum 'StrEnum'> first_enum = <enum 'StrEnum'> item = ('Omega', 20) member_name = 'Omega' member_value = 20 metacls = <class 'enum.EnumType'> module = 'test_native_enum' names = [('Alpha', 10), ('Omega', 20)] qualname = None start = 1 type = None /usr/lib/python3.12/enum.py:606: in __new__ raise exc.with_traceback(tb) __class__ = <class 'enum.EnumType'> __new__ = <function StrEnum.__new__ at 0x701ec1711da0> _gnv = <staticmethod(<function StrEnum._generate_next_value_ at 0x701ec1711e40>)> _order_ = None _simple = False bases = (<enum 'StrEnum'>,) boundary = None classdict = {'Alpha': <enum._proto_member object at 0x701ebc74f9b0>, 'Omega': <enum._proto_member object at 0x701ebc74cce0>, '__module__': 'test_native_enum', '_all_bits_': 0, ...} cls = 'greek' exc = TypeError('10 is not a string') first_enum = <enum 'StrEnum'> ignore = ['_ignore_'] invalid_names = set() key = '_ignore_' kwds = {} member_names = {'Alpha': None, 'Omega': None} member_type = <class 'str'> metacls = <class 'enum.EnumType'> name = 'Omega' save_new = False tb = <traceback object at 0x701ebc7a6cc0> use_args = True value = 20 /usr/lib/python3.12/enum.py:596: in __new__ enum_class = super().__new__(metacls, cls, bases, classdict, **kwds) __class__ = <class 'enum.EnumType'> __new__ = <function StrEnum.__new__ at 0x701ec1711da0> _gnv = <staticmethod(<function StrEnum._generate_next_value_ at 0x701ec1711e40>)> _order_ = None _simple = False bases = (<enum 'StrEnum'>,) boundary = None classdict = {'Alpha': <enum._proto_member object at 0x701ebc74f9b0>, 'Omega': <enum._proto_member object at 0x701ebc74cce0>, '__module__': 'test_native_enum', '_all_bits_': 0, ...} cls = 'greek' exc = TypeError('10 is not a string') first_enum = <enum 'StrEnum'> ignore = ['_ignore_'] invalid_names = set() key = '_ignore_' kwds = {} member_names = {'Alpha': None, 'Omega': None} member_type = <class 'str'> metacls = <class 'enum.EnumType'> name = 'Omega' save_new = False tb = <traceback object at 0x701ebc7a6cc0> use_args = True value = 20 /usr/lib/python3.12/enum.py:271: in __set_name__ enum_member = enum_class._new_member_(enum_class, *args) args = (10,) enum_class = <enum 'greek'> member_name = 'Alpha' self = <enum._proto_member object at 0x701ebc74f9b0> value = 10 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ cls = <enum 'greek'>, values = (10,) def __new__(cls, *values): "values must already be of type `str`" if len(values) > 3: raise TypeError('too many arguments for str(): %r' % (values, )) if len(values) == 1: # it must be a string if not isinstance(values[0], str): > raise TypeError('%r is not a string' % (values[0], )) E TypeError: 10 is not a string cls = <enum 'greek'> values = (10,) /usr/lib/python3.12/enum.py:1322: TypeError =========================== short test summary info ============================ FAILED test_native_enum.py::test_native_enum_StrEnum_greek - TypeError: 10 is... ======================= 1 failed, 39 deselected in 0.07s ======================= ERROR: completed_process.returncode=1 ``` * Remove StrEnum code. * Make enum_kind::Enum the default kind. * Catch redundant .export_values() calls. * [ci skip] Add back original documentation for `py::enum_` under new advanced/deprecated.rst * [ci skip] Add documentation for `py::enum_kind` and `py::detail::type_caster_enum_type_enabled` * Rename `Type` to `EnumType` for readability. * Eliminate py::enum_kind, use "enum.Enum", "enum.IntEnum" directly. This is still WIP. * EXPERIMENTAL StrEnum code. To be removed. * Remove experimental StrEnum code: My judgement: Supporting StrEnum is maybe nice, but not very valuable. I don't think it is worth the extra C++ code. A level of indirection would need to be managed, e.g. RED ↔ Python "r" ↔ C++ 0 Green ↔ Python "g" ↔ C++ 1 These mappings would need to be stored and processed. * Add test with enum.IntFlag (no production code changes required). * First import_or_getattr() implementation (dedicated tests are still missing). * Fix import_or_getattr() implementation, add tests, fix clang-tidy errors. * [ci skip] Update classes.rst: replace `py::enum_kind` with `native_type_name` * For "constructor similar to that of enum.Enum" point to https://docs.python.org/3/howto/enum.html#functional-api, as suggested by gh-timohl (https://github.com/pybind/pybind11/pull/5555#discussion_r2009277507). * Advertise Enum, IntEnum, Flag, IntFlags are compatible stdlib enum types in the documentation (as suggested by gh-timohl, https://github.com/pybind/pybind11/pull/5555#pullrequestreview-2708832587); add test for enum.Flag to ensure that is actually true.
This commit is contained in:
committed by
GitHub
parent
48eb5ad9b9
commit
f365314ec0
@@ -136,6 +136,7 @@ set(PYBIND11_HEADERS
|
||||
include/pybind11/detail/dynamic_raw_ptr_cast_if_possible.h
|
||||
include/pybind11/detail/init.h
|
||||
include/pybind11/detail/internals.h
|
||||
include/pybind11/detail/native_enum_data.h
|
||||
include/pybind11/detail/struct_smart_holder.h
|
||||
include/pybind11/detail/type_caster_base.h
|
||||
include/pybind11/detail/typeid.h
|
||||
@@ -162,6 +163,7 @@ set(PYBIND11_HEADERS
|
||||
include/pybind11/gil_safe_call_once.h
|
||||
include/pybind11/iostream.h
|
||||
include/pybind11/functional.h
|
||||
include/pybind11/native_enum.h
|
||||
include/pybind11/numpy.h
|
||||
include/pybind11/operators.h
|
||||
include/pybind11/pybind11.h
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
.. _custom_type_caster:
|
||||
|
||||
Custom type casters
|
||||
===================
|
||||
|
||||
|
||||
113
docs/advanced/deprecated.rst
Normal file
113
docs/advanced/deprecated.rst
Normal file
@@ -0,0 +1,113 @@
|
||||
Deprecated
|
||||
##########
|
||||
|
||||
.. _deprecated_enum:
|
||||
|
||||
``py::enum_``
|
||||
=============
|
||||
|
||||
This is the original documentation for ``py::enum_``, which is deprecated
|
||||
because it is not `PEP 435 compatible <https://peps.python.org/pep-0435/>`_
|
||||
(see also `#2332 <https://github.com/pybind/pybind11/issues/2332>`_).
|
||||
Please prefer ``py::native_enum`` (added with pybind11v3) when writing
|
||||
new bindings. See :ref:`native_enum` for more information.
|
||||
|
||||
Let's suppose that we have an example class that contains internal types
|
||||
like enumerations, e.g.:
|
||||
|
||||
.. code-block:: cpp
|
||||
|
||||
struct Pet {
|
||||
enum Kind {
|
||||
Dog = 0,
|
||||
Cat
|
||||
};
|
||||
|
||||
struct Attributes {
|
||||
float age = 0;
|
||||
};
|
||||
|
||||
Pet(const std::string &name, Kind type) : name(name), type(type) { }
|
||||
|
||||
std::string name;
|
||||
Kind type;
|
||||
Attributes attr;
|
||||
};
|
||||
|
||||
The binding code for this example looks as follows:
|
||||
|
||||
.. code-block:: cpp
|
||||
|
||||
py::class_<Pet> pet(m, "Pet");
|
||||
|
||||
pet.def(py::init<const std::string &, Pet::Kind>())
|
||||
.def_readwrite("name", &Pet::name)
|
||||
.def_readwrite("type", &Pet::type)
|
||||
.def_readwrite("attr", &Pet::attr);
|
||||
|
||||
py::enum_<Pet::Kind>(pet, "Kind")
|
||||
.value("Dog", Pet::Kind::Dog)
|
||||
.value("Cat", Pet::Kind::Cat)
|
||||
.export_values();
|
||||
|
||||
py::class_<Pet::Attributes>(pet, "Attributes")
|
||||
.def(py::init<>())
|
||||
.def_readwrite("age", &Pet::Attributes::age);
|
||||
|
||||
|
||||
To ensure that the nested types ``Kind`` and ``Attributes`` are created within the scope of ``Pet``, the
|
||||
``pet`` ``py::class_`` instance must be supplied to the :class:`enum_` and ``py::class_``
|
||||
constructor. The :func:`enum_::export_values` function exports the enum entries
|
||||
into the parent scope, which should be skipped for newer C++11-style strongly
|
||||
typed enums.
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> p = Pet("Lucy", Pet.Cat)
|
||||
>>> p.type
|
||||
Kind.Cat
|
||||
>>> int(p.type)
|
||||
1L
|
||||
|
||||
The entries defined by the enumeration type are exposed in the ``__members__`` property:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> Pet.Kind.__members__
|
||||
{'Dog': Kind.Dog, 'Cat': Kind.Cat}
|
||||
|
||||
The ``name`` property returns the name of the enum value as a unicode string.
|
||||
|
||||
.. note::
|
||||
|
||||
It is also possible to use ``str(enum)``, however these accomplish different
|
||||
goals. The following shows how these two approaches differ.
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> p = Pet("Lucy", Pet.Cat)
|
||||
>>> pet_type = p.type
|
||||
>>> pet_type
|
||||
Pet.Cat
|
||||
>>> str(pet_type)
|
||||
'Pet.Cat'
|
||||
>>> pet_type.name
|
||||
'Cat'
|
||||
|
||||
.. note::
|
||||
|
||||
When the special tag ``py::arithmetic()`` is specified to the ``enum_``
|
||||
constructor, pybind11 creates an enumeration that also supports rudimentary
|
||||
arithmetic and bit-level operations like comparisons, and, or, xor, negation,
|
||||
etc.
|
||||
|
||||
.. code-block:: cpp
|
||||
|
||||
py::enum_<Pet::Kind>(pet, "Kind", py::arithmetic())
|
||||
...
|
||||
|
||||
By default, these are omitted to conserve space.
|
||||
|
||||
.. warning::
|
||||
|
||||
Contrary to Python customs, enum values from the wrappers should not be compared using ``is``, but with ``==`` (see `#1177 <https://github.com/pybind/pybind11/issues/1177>`_ for background).
|
||||
116
docs/classes.rst
116
docs/classes.rst
@@ -459,6 +459,8 @@ you can use ``py::detail::overload_cast_impl`` with an additional set of parenth
|
||||
other using the ``.def(py::init<...>())`` syntax. The existing machinery
|
||||
for specifying keyword and default arguments also works.
|
||||
|
||||
.. _native_enum:
|
||||
|
||||
Enumerations and internal types
|
||||
===============================
|
||||
|
||||
@@ -487,6 +489,8 @@ The binding code for this example looks as follows:
|
||||
|
||||
.. code-block:: cpp
|
||||
|
||||
#include <pybind11/native_enum.h> // Not already included with pybind11.h
|
||||
|
||||
py::class_<Pet> pet(m, "Pet");
|
||||
|
||||
pet.def(py::init<const std::string &, Pet::Kind>())
|
||||
@@ -494,69 +498,93 @@ The binding code for this example looks as follows:
|
||||
.def_readwrite("type", &Pet::type)
|
||||
.def_readwrite("attr", &Pet::attr);
|
||||
|
||||
py::enum_<Pet::Kind>(pet, "Kind")
|
||||
py::native_enum<Pet::Kind>(pet, "Kind")
|
||||
.value("Dog", Pet::Kind::Dog)
|
||||
.value("Cat", Pet::Kind::Cat)
|
||||
.export_values();
|
||||
.export_values()
|
||||
.finalize();
|
||||
|
||||
py::class_<Pet::Attributes>(pet, "Attributes")
|
||||
.def(py::init<>())
|
||||
.def_readwrite("age", &Pet::Attributes::age);
|
||||
|
||||
|
||||
To ensure that the nested types ``Kind`` and ``Attributes`` are created within the scope of ``Pet``, the
|
||||
``pet`` ``py::class_`` instance must be supplied to the :class:`enum_` and ``py::class_``
|
||||
constructor. The :func:`enum_::export_values` function exports the enum entries
|
||||
into the parent scope, which should be skipped for newer C++11-style strongly
|
||||
typed enums.
|
||||
To ensure that the nested types ``Kind`` and ``Attributes`` are created
|
||||
within the scope of ``Pet``, the ``pet`` ``py::class_`` instance must be
|
||||
supplied to the ``py::native_enum`` and ``py::class_`` constructors. The
|
||||
``.export_values()`` function is available for exporting the enum entries
|
||||
into the parent scope, if desired.
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> p = Pet("Lucy", Pet.Cat)
|
||||
>>> p.type
|
||||
Kind.Cat
|
||||
>>> int(p.type)
|
||||
1L
|
||||
|
||||
The entries defined by the enumeration type are exposed in the ``__members__`` property:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> Pet.Kind.__members__
|
||||
{'Dog': Kind.Dog, 'Cat': Kind.Cat}
|
||||
|
||||
The ``name`` property returns the name of the enum value as a unicode string.
|
||||
``py::native_enum`` was introduced with pybind11v3. It binds C++ enum types
|
||||
to native Python enum types, typically types in Python's
|
||||
`stdlib enum <https://docs.python.org/3/library/enum.html>`_ module,
|
||||
which are `PEP 435 compatible <https://peps.python.org/pep-0435/>`_.
|
||||
This is the recommended way to bind C++ enums.
|
||||
The older ``py::enum_`` is not PEP 435 compatible
|
||||
(see `issue #2332 <https://github.com/pybind/pybind11/issues/2332>`_)
|
||||
but remains supported indefinitely for backward compatibility.
|
||||
New bindings should prefer ``py::native_enum``.
|
||||
|
||||
.. note::
|
||||
|
||||
It is also possible to use ``str(enum)``, however these accomplish different
|
||||
goals. The following shows how these two approaches differ.
|
||||
The deprecated ``py::enum_`` is :ref:`documented here <deprecated_enum>`.
|
||||
|
||||
.. code-block:: pycon
|
||||
The ``.finalize()`` call above is needed because Python's native enums
|
||||
cannot be built incrementally — all name/value pairs need to be passed at
|
||||
once. To achieve this, ``py::native_enum`` acts as a buffer to collect the
|
||||
name/value pairs. The ``.finalize()`` call uses the accumulated name/value
|
||||
pairs to build the arguments for constructing a native Python enum type.
|
||||
|
||||
>>> p = Pet("Lucy", Pet.Cat)
|
||||
>>> pet_type = p.type
|
||||
>>> pet_type
|
||||
Pet.Cat
|
||||
>>> str(pet_type)
|
||||
'Pet.Cat'
|
||||
>>> pet_type.name
|
||||
'Cat'
|
||||
The ``py::native_enum`` constructor supports a third optional
|
||||
``native_type_name`` string argument, with default value ``"enum.Enum"``.
|
||||
Other types can be specified like this:
|
||||
|
||||
.. code-block:: cpp
|
||||
|
||||
py::native_enum<Pet::Kind>(pet, "Kind", "enum.IntEnum")
|
||||
|
||||
Any fully-qualified Python name can be specified. The only requirement is
|
||||
that the named type is similar to
|
||||
`enum.Enum <https://docs.python.org/3/library/enum.html#enum.Enum>`_
|
||||
in these ways:
|
||||
|
||||
* Has a `constructor similar to that of enum.Enum
|
||||
<https://docs.python.org/3/howto/enum.html#functional-api>`_::
|
||||
|
||||
Colors = enum.Enum("Colors", (("Red", 0), ("Green", 1)))
|
||||
|
||||
* A `C++ underlying <https://en.cppreference.com/w/cpp/types/underlying_type>`_
|
||||
enum value can be passed to the constructor for the Python enum value::
|
||||
|
||||
red = Colors(0)
|
||||
|
||||
* The enum values have a ``.value`` property yielding a value that
|
||||
can be cast to the C++ underlying type::
|
||||
|
||||
underlying = red.value
|
||||
|
||||
As of Python 3.13, the compatible `types in the stdlib enum module
|
||||
<https://docs.python.org/3/library/enum.html#module-contents>`_ are:
|
||||
``Enum``, ``IntEnum``, ``Flag``, ``IntFlag``.
|
||||
|
||||
.. note::
|
||||
|
||||
When the special tag ``py::arithmetic()`` is specified to the ``enum_``
|
||||
constructor, pybind11 creates an enumeration that also supports rudimentary
|
||||
arithmetic and bit-level operations like comparisons, and, or, xor, negation,
|
||||
etc.
|
||||
In rare cases, a C++ enum may be bound to Python via a
|
||||
:ref:`custom type caster <custom_type_caster>`. In such cases, a
|
||||
template specialization like this may be required:
|
||||
|
||||
.. code-block:: cpp
|
||||
|
||||
py::enum_<Pet::Kind>(pet, "Kind", py::arithmetic())
|
||||
...
|
||||
#if defined(PYBIND11_HAS_NATIVE_ENUM)
|
||||
namespace pybind11::detail {
|
||||
template <typename FancyEnum>
|
||||
struct type_caster_enum_type_enabled<
|
||||
FancyEnum,
|
||||
std::enable_if_t<is_fancy_enum<FancyEnum>::value>> : std::false_type {};
|
||||
}
|
||||
#endif
|
||||
|
||||
By default, these are omitted to conserve space.
|
||||
This specialization is needed only if the custom type caster is templated.
|
||||
|
||||
.. warning::
|
||||
|
||||
Contrary to Python customs, enum values from the wrappers should not be compared using ``is``, but with ``==`` (see `#1177 <https://github.com/pybind/pybind11/issues/1177>`_ for background).
|
||||
The ``PYBIND11_HAS_NATIVE_ENUM`` guard is needed only if backward
|
||||
compatibility with pybind11v2 is required.
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
advanced/pycpp/index
|
||||
advanced/embedding
|
||||
advanced/misc
|
||||
advanced/deprecated
|
||||
|
||||
.. toctree::
|
||||
:caption: Extra Information
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
#include "detail/common.h"
|
||||
#include "detail/descr.h"
|
||||
#include "detail/native_enum_data.h"
|
||||
#include "detail/type_caster_base.h"
|
||||
#include "detail/typeid.h"
|
||||
#include "pytypes.h"
|
||||
@@ -53,6 +54,104 @@ cast_op(make_caster<T> &&caster) {
|
||||
return std::move(caster).operator result_t();
|
||||
}
|
||||
|
||||
template <typename EnumType>
|
||||
class type_caster_enum_type {
|
||||
private:
|
||||
using Underlying = typename std::underlying_type<EnumType>::type;
|
||||
|
||||
public:
|
||||
static constexpr auto name = const_name<EnumType>();
|
||||
|
||||
template <typename SrcType>
|
||||
static handle cast(SrcType &&src, return_value_policy, handle parent) {
|
||||
handle native_enum
|
||||
= global_internals_native_enum_type_map_get_item(std::type_index(typeid(EnumType)));
|
||||
if (native_enum) {
|
||||
return native_enum(static_cast<Underlying>(src)).release();
|
||||
}
|
||||
return type_caster_base<EnumType>::cast(
|
||||
std::forward<SrcType>(src),
|
||||
// Fixes https://github.com/pybind/pybind11/pull/3643#issuecomment-1022987818:
|
||||
return_value_policy::copy,
|
||||
parent);
|
||||
}
|
||||
|
||||
bool load(handle src, bool convert) {
|
||||
handle native_enum
|
||||
= global_internals_native_enum_type_map_get_item(std::type_index(typeid(EnumType)));
|
||||
if (native_enum) {
|
||||
if (!isinstance(src, native_enum)) {
|
||||
return false;
|
||||
}
|
||||
type_caster<Underlying> underlying_caster;
|
||||
if (!underlying_caster.load(src.attr("value"), convert)) {
|
||||
pybind11_fail("native_enum internal consistency failure.");
|
||||
}
|
||||
value = static_cast<EnumType>(static_cast<Underlying>(underlying_caster));
|
||||
return true;
|
||||
}
|
||||
if (!pybind11_enum_) {
|
||||
pybind11_enum_.reset(new type_caster_base<EnumType>());
|
||||
}
|
||||
return pybind11_enum_->load(src, convert);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
using cast_op_type = detail::cast_op_type<T>;
|
||||
|
||||
// NOLINTNEXTLINE(google-explicit-constructor)
|
||||
operator EnumType *() {
|
||||
if (!pybind11_enum_) {
|
||||
return &value;
|
||||
}
|
||||
return pybind11_enum_->operator EnumType *();
|
||||
}
|
||||
|
||||
// NOLINTNEXTLINE(google-explicit-constructor)
|
||||
operator EnumType &() {
|
||||
if (!pybind11_enum_) {
|
||||
return value;
|
||||
}
|
||||
return pybind11_enum_->operator EnumType &();
|
||||
}
|
||||
|
||||
private:
|
||||
std::unique_ptr<type_caster_base<EnumType>> pybind11_enum_;
|
||||
EnumType value;
|
||||
};
|
||||
|
||||
template <typename EnumType, typename SFINAE = void>
|
||||
struct type_caster_enum_type_enabled : std::true_type {};
|
||||
|
||||
template <typename T>
|
||||
struct type_uses_type_caster_enum_type {
|
||||
static constexpr bool value
|
||||
= std::is_enum<T>::value && type_caster_enum_type_enabled<T>::value;
|
||||
};
|
||||
|
||||
template <typename EnumType>
|
||||
class type_caster<EnumType, detail::enable_if_t<type_uses_type_caster_enum_type<EnumType>::value>>
|
||||
: public type_caster_enum_type<EnumType> {};
|
||||
|
||||
template <typename T, detail::enable_if_t<std::is_enum<T>::value, int> = 0>
|
||||
bool isinstance_native_enum_impl(handle obj, const std::type_info &tp) {
|
||||
handle native_enum = global_internals_native_enum_type_map_get_item(tp);
|
||||
if (!native_enum) {
|
||||
return false;
|
||||
}
|
||||
return isinstance(obj, native_enum);
|
||||
}
|
||||
|
||||
template <typename T, detail::enable_if_t<!std::is_enum<T>::value, int> = 0>
|
||||
bool isinstance_native_enum_impl(handle, const std::type_info &) {
|
||||
return false;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
bool isinstance_native_enum(handle obj, const std::type_info &tp) {
|
||||
return isinstance_native_enum_impl<intrinsic_t<T>>(obj, tp);
|
||||
}
|
||||
|
||||
template <typename type>
|
||||
class type_caster<std::reference_wrapper<type>> {
|
||||
private:
|
||||
@@ -1470,8 +1569,17 @@ template <typename T,
|
||||
= 0>
|
||||
T cast(const handle &handle) {
|
||||
using namespace detail;
|
||||
static_assert(!cast_is_temporary_value_reference<T>::value,
|
||||
constexpr bool is_enum_cast = type_uses_type_caster_enum_type<intrinsic_t<T>>::value;
|
||||
static_assert(!cast_is_temporary_value_reference<T>::value || is_enum_cast,
|
||||
"Unable to cast type to reference: value is local to type caster");
|
||||
#ifndef NDEBUG
|
||||
if (is_enum_cast && cast_is_temporary_value_reference<T>::value) {
|
||||
if (detail::global_internals_native_enum_type_map_contains(
|
||||
std::type_index(typeid(intrinsic_t<T>)))) {
|
||||
pybind11_fail("Unable to cast native enum type to reference");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
return cast_op<T>(load_type<T>(handle));
|
||||
}
|
||||
|
||||
|
||||
@@ -716,15 +716,7 @@ inline PyObject *make_new_python_type(const type_record &rec) {
|
||||
PyUnicode_FromFormat("%U.%U", rec.scope.attr("__qualname__").ptr(), name.ptr()));
|
||||
}
|
||||
|
||||
object module_;
|
||||
if (rec.scope) {
|
||||
if (hasattr(rec.scope, "__module__")) {
|
||||
module_ = rec.scope.attr("__module__");
|
||||
} else if (hasattr(rec.scope, "__name__")) {
|
||||
module_ = rec.scope.attr("__name__");
|
||||
}
|
||||
}
|
||||
|
||||
object module_ = get_module_name_if_available(rec.scope);
|
||||
const auto *full_name = c_str(
|
||||
#if !defined(PYPY_VERSION)
|
||||
module_ ? str(module_).cast<std::string>() + "." + rec.name :
|
||||
|
||||
@@ -37,11 +37,11 @@
|
||||
/// further ABI-incompatible changes may be made before the ABI is officially
|
||||
/// changed to the new version.
|
||||
#ifndef PYBIND11_INTERNALS_VERSION
|
||||
# define PYBIND11_INTERNALS_VERSION 7
|
||||
# define PYBIND11_INTERNALS_VERSION 8
|
||||
#endif
|
||||
|
||||
#if PYBIND11_INTERNALS_VERSION < 7
|
||||
# error "PYBIND11_INTERNALS_VERSION 7 is the minimum for all platforms for pybind11v3."
|
||||
#if PYBIND11_INTERNALS_VERSION < 8
|
||||
# error "PYBIND11_INTERNALS_VERSION 8 is the minimum for all platforms for pybind11v3."
|
||||
#endif
|
||||
|
||||
PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE)
|
||||
@@ -194,6 +194,8 @@ struct internals {
|
||||
// We want unique addresses since we use pointer equality to compare function records
|
||||
std::string function_record_capsule_name = internals_function_record_capsule_name;
|
||||
|
||||
type_map<PyObject *> native_enum_type_map;
|
||||
|
||||
internals() = default;
|
||||
internals(const internals &other) = delete;
|
||||
internals &operator=(const internals &other) = delete;
|
||||
|
||||
201
include/pybind11/detail/native_enum_data.h
Normal file
201
include/pybind11/detail/native_enum_data.h
Normal file
@@ -0,0 +1,201 @@
|
||||
// Copyright (c) 2022-2025 The pybind Community.
|
||||
// All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
#pragma once
|
||||
|
||||
#define PYBIND11_HAS_NATIVE_ENUM
|
||||
|
||||
#include "../pytypes.h"
|
||||
#include "common.h"
|
||||
#include "internals.h"
|
||||
|
||||
#include <cassert>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <typeindex>
|
||||
|
||||
PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE)
|
||||
PYBIND11_NAMESPACE_BEGIN(detail)
|
||||
|
||||
// This is a separate function only to enable easy unit testing.
|
||||
inline std::string
|
||||
native_enum_missing_finalize_error_message(const std::string &enum_name_encoded) {
|
||||
return "pybind11::native_enum<...>(\"" + enum_name_encoded + "\", ...): MISSING .finalize()";
|
||||
}
|
||||
|
||||
class native_enum_data {
|
||||
public:
|
||||
native_enum_data(const object &parent_scope,
|
||||
const char *enum_name,
|
||||
const char *native_type_name,
|
||||
const std::type_index &enum_type_index)
|
||||
: enum_name_encoded{enum_name}, native_type_name_encoded{native_type_name},
|
||||
enum_type_index{enum_type_index}, parent_scope(parent_scope), enum_name{enum_name},
|
||||
native_type_name{native_type_name}, export_values_flag{false}, finalize_needed{false} {}
|
||||
|
||||
void finalize();
|
||||
|
||||
native_enum_data(const native_enum_data &) = delete;
|
||||
native_enum_data &operator=(const native_enum_data &) = delete;
|
||||
|
||||
#if !defined(NDEBUG)
|
||||
// This dtor cannot easily be unit tested because it terminates the process.
|
||||
~native_enum_data() {
|
||||
if (finalize_needed) {
|
||||
pybind11_fail(native_enum_missing_finalize_error_message(enum_name_encoded));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
protected:
|
||||
void disarm_finalize_check(const char *error_context) {
|
||||
if (!finalize_needed) {
|
||||
pybind11_fail("pybind11::native_enum<...>(\"" + enum_name_encoded
|
||||
+ "\"): " + error_context);
|
||||
}
|
||||
finalize_needed = false;
|
||||
}
|
||||
|
||||
void arm_finalize_check() {
|
||||
assert(!finalize_needed); // Catch redundant calls.
|
||||
finalize_needed = true;
|
||||
}
|
||||
|
||||
std::string enum_name_encoded;
|
||||
std::string native_type_name_encoded;
|
||||
std::type_index enum_type_index;
|
||||
|
||||
private:
|
||||
object parent_scope;
|
||||
str enum_name;
|
||||
str native_type_name;
|
||||
|
||||
protected:
|
||||
list members;
|
||||
list docs;
|
||||
bool export_values_flag : 1; // Attention: It is best to keep the bools together.
|
||||
|
||||
private:
|
||||
bool finalize_needed : 1;
|
||||
};
|
||||
|
||||
inline void global_internals_native_enum_type_map_set_item(const std::type_index &enum_type_index,
|
||||
PyObject *py_enum) {
|
||||
with_internals(
|
||||
[&](internals &internals) { internals.native_enum_type_map[enum_type_index] = py_enum; });
|
||||
}
|
||||
|
||||
inline handle
|
||||
global_internals_native_enum_type_map_get_item(const std::type_index &enum_type_index) {
|
||||
return with_internals([&](internals &internals) {
|
||||
auto found = internals.native_enum_type_map.find(enum_type_index);
|
||||
if (found != internals.native_enum_type_map.end()) {
|
||||
return handle(found->second);
|
||||
}
|
||||
return handle();
|
||||
});
|
||||
}
|
||||
|
||||
inline bool
|
||||
global_internals_native_enum_type_map_contains(const std::type_index &enum_type_index) {
|
||||
return with_internals([&](internals &internals) {
|
||||
return internals.native_enum_type_map.count(enum_type_index) != 0;
|
||||
});
|
||||
}
|
||||
|
||||
inline object import_or_getattr(const std::string &fully_qualified_name,
|
||||
const std::string &append_to_exception_message) {
|
||||
std::istringstream stream(fully_qualified_name);
|
||||
std::string part;
|
||||
|
||||
if (!std::getline(stream, part, '.') || part.empty()) {
|
||||
std::string msg = "Invalid fully-qualified name `";
|
||||
msg += fully_qualified_name;
|
||||
msg += "`";
|
||||
msg += append_to_exception_message;
|
||||
throw value_error(msg);
|
||||
}
|
||||
|
||||
auto curr_scope = reinterpret_steal<object>(PyImport_ImportModule(part.c_str()));
|
||||
if (!curr_scope) {
|
||||
std::string msg = "Failed to import top-level module `";
|
||||
msg += part;
|
||||
msg += "`";
|
||||
msg += append_to_exception_message;
|
||||
raise_from(PyExc_ImportError, msg.c_str());
|
||||
throw error_already_set();
|
||||
}
|
||||
|
||||
// Now recursively getattr or import remaining parts
|
||||
std::string curr_path = part;
|
||||
while (std::getline(stream, part, '.')) {
|
||||
if (part.empty()) {
|
||||
std::string msg = "Invalid fully-qualified name `";
|
||||
msg += fully_qualified_name;
|
||||
msg += "`";
|
||||
msg += append_to_exception_message;
|
||||
throw value_error(msg);
|
||||
}
|
||||
std::string next_path = curr_path;
|
||||
next_path += ".";
|
||||
next_path += part;
|
||||
auto next_scope
|
||||
= reinterpret_steal<object>(PyObject_GetAttrString(curr_scope.ptr(), part.c_str()));
|
||||
if (!next_scope) {
|
||||
error_fetch_and_normalize stored_getattr_error("getattr");
|
||||
// Try importing the next level
|
||||
next_scope = reinterpret_steal<object>(PyImport_ImportModule(next_path.c_str()));
|
||||
if (!next_scope) {
|
||||
error_fetch_and_normalize stored_import_error("import");
|
||||
std::string msg = "Failed to import or getattr `";
|
||||
msg += part;
|
||||
msg += "` from `";
|
||||
msg += curr_path;
|
||||
msg += "`";
|
||||
msg += append_to_exception_message;
|
||||
msg += "\n-------- getattr exception --------\n";
|
||||
msg += stored_getattr_error.error_string();
|
||||
msg += "\n-------- import exception --------\n";
|
||||
msg += stored_import_error.error_string();
|
||||
throw import_error(msg.c_str());
|
||||
}
|
||||
}
|
||||
curr_scope = next_scope;
|
||||
curr_path = next_path;
|
||||
}
|
||||
return curr_scope;
|
||||
}
|
||||
|
||||
inline void native_enum_data::finalize() {
|
||||
disarm_finalize_check("DOUBLE finalize");
|
||||
if (hasattr(parent_scope, enum_name)) {
|
||||
pybind11_fail("pybind11::native_enum<...>(\"" + enum_name_encoded
|
||||
+ "\"): an object with that name is already defined");
|
||||
}
|
||||
auto py_enum_type = import_or_getattr(native_type_name, " (native_type_name)");
|
||||
auto py_enum = py_enum_type(enum_name, members);
|
||||
object module_name = get_module_name_if_available(parent_scope);
|
||||
if (module_name) {
|
||||
py_enum.attr("__module__") = module_name;
|
||||
}
|
||||
parent_scope.attr(enum_name) = py_enum;
|
||||
if (export_values_flag) {
|
||||
for (auto member : members) {
|
||||
auto member_name = member[int_(0)];
|
||||
if (hasattr(parent_scope, member_name)) {
|
||||
pybind11_fail("pybind11::native_enum<...>(\"" + enum_name_encoded + "\").value(\""
|
||||
+ member_name.cast<std::string>()
|
||||
+ "\"): an object with that name is already defined");
|
||||
}
|
||||
parent_scope.attr(member_name) = py_enum[member_name];
|
||||
}
|
||||
}
|
||||
for (auto doc : docs) {
|
||||
py_enum[doc[int_(0)]].attr("__doc__") = doc[int_(1)];
|
||||
}
|
||||
global_internals_native_enum_type_map_set_item(enum_type_index, py_enum.release().ptr());
|
||||
}
|
||||
|
||||
PYBIND11_NAMESPACE_END(detail)
|
||||
PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE)
|
||||
66
include/pybind11/native_enum.h
Normal file
66
include/pybind11/native_enum.h
Normal file
@@ -0,0 +1,66 @@
|
||||
// Copyright (c) 2022-2025 The pybind Community.
|
||||
// All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "detail/common.h"
|
||||
#include "detail/native_enum_data.h"
|
||||
#include "detail/type_caster_base.h"
|
||||
#include "cast.h"
|
||||
|
||||
#include <cassert>
|
||||
#include <limits>
|
||||
#include <type_traits>
|
||||
#include <typeindex>
|
||||
|
||||
PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE)
|
||||
|
||||
/// Conversions between Python's native (stdlib) enum types and C++ enums.
|
||||
template <typename EnumType>
|
||||
class native_enum : public detail::native_enum_data {
|
||||
public:
|
||||
using Underlying = typename std::underlying_type<EnumType>::type;
|
||||
|
||||
native_enum(const object &parent_scope,
|
||||
const char *name,
|
||||
const char *native_type_name = "enum.Enum")
|
||||
: detail::native_enum_data(
|
||||
parent_scope, name, native_type_name, std::type_index(typeid(EnumType))) {
|
||||
if (detail::get_local_type_info(typeid(EnumType)) != nullptr
|
||||
|| detail::get_global_type_info(typeid(EnumType)) != nullptr) {
|
||||
pybind11_fail(
|
||||
"pybind11::native_enum<...>(\"" + enum_name_encoded
|
||||
+ "\") is already registered as a `pybind11::enum_` or `pybind11::class_`!");
|
||||
}
|
||||
if (detail::global_internals_native_enum_type_map_contains(enum_type_index)) {
|
||||
pybind11_fail("pybind11::native_enum<...>(\"" + enum_name_encoded
|
||||
+ "\") is already registered!");
|
||||
}
|
||||
arm_finalize_check();
|
||||
}
|
||||
|
||||
/// Export enumeration entries into the parent scope
|
||||
native_enum &export_values() {
|
||||
assert(!export_values_flag); // Catch redundant calls.
|
||||
export_values_flag = true;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/// Add an enumeration entry
|
||||
native_enum &value(char const *name, EnumType value, const char *doc = nullptr) {
|
||||
// Disarm for the case that the native_enum_data dtor runs during exception unwinding.
|
||||
disarm_finalize_check("value after finalize");
|
||||
members.append(make_tuple(name, static_cast<Underlying>(value)));
|
||||
if (doc) {
|
||||
docs.append(make_tuple(name, doc));
|
||||
}
|
||||
arm_finalize_check(); // There was no exception.
|
||||
return *this;
|
||||
}
|
||||
|
||||
native_enum(const native_enum &) = delete;
|
||||
native_enum &operator=(const native_enum &) = delete;
|
||||
};
|
||||
|
||||
PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE)
|
||||
@@ -13,6 +13,7 @@
|
||||
#include "detail/dynamic_raw_ptr_cast_if_possible.h"
|
||||
#include "detail/exception_translation.h"
|
||||
#include "detail/init.h"
|
||||
#include "detail/native_enum_data.h"
|
||||
#include "detail/using_smart_holder.h"
|
||||
#include "attr.h"
|
||||
#include "gil.h"
|
||||
@@ -2768,6 +2769,14 @@ public:
|
||||
template <typename... Extra>
|
||||
enum_(const handle &scope, const char *name, const Extra &...extra)
|
||||
: class_<Type>(scope, name, extra...), m_base(*this, scope) {
|
||||
{
|
||||
if (detail::global_internals_native_enum_type_map_contains(
|
||||
std::type_index(typeid(Type)))) {
|
||||
pybind11_fail("pybind11::enum_ \"" + std::string(name)
|
||||
+ "\" is already registered as a pybind11::native_enum!");
|
||||
}
|
||||
}
|
||||
|
||||
constexpr bool is_arithmetic = detail::any_of<std::is_same<arithmetic, Extra>...>::value;
|
||||
constexpr bool is_convertible = std::is_convertible<Type, Underlying>::value;
|
||||
m_base.init(is_arithmetic, is_convertible);
|
||||
|
||||
@@ -48,6 +48,9 @@ PYBIND11_NAMESPACE_BEGIN(detail)
|
||||
class args_proxy;
|
||||
bool isinstance_generic(handle obj, const std::type_info &tp);
|
||||
|
||||
template <typename T>
|
||||
bool isinstance_native_enum(handle obj, const std::type_info &tp);
|
||||
|
||||
// Accessor forward declarations
|
||||
template <typename Policy>
|
||||
class accessor;
|
||||
@@ -852,7 +855,8 @@ bool isinstance(handle obj) {
|
||||
|
||||
template <typename T, detail::enable_if_t<!std::is_base_of<object, T>::value, int> = 0>
|
||||
bool isinstance(handle obj) {
|
||||
return detail::isinstance_generic(obj, typeid(T));
|
||||
return detail::isinstance_native_enum<T>(obj, typeid(T))
|
||||
|| detail::isinstance_generic(obj, typeid(T));
|
||||
}
|
||||
|
||||
template <>
|
||||
@@ -2644,5 +2648,18 @@ PYBIND11_MATH_OPERATOR_BINARY_INPLACE(operator>>=, PyNumber_InPlaceRshift)
|
||||
#undef PYBIND11_MATH_OPERATOR_BINARY
|
||||
#undef PYBIND11_MATH_OPERATOR_BINARY_INPLACE
|
||||
|
||||
// Meant to return a Python str, but this is not checked.
|
||||
inline object get_module_name_if_available(handle scope) {
|
||||
if (scope) {
|
||||
if (hasattr(scope, "__module__")) {
|
||||
return scope.attr("__module__");
|
||||
}
|
||||
if (hasattr(scope, "__name__")) {
|
||||
return scope.attr("__name__");
|
||||
}
|
||||
}
|
||||
return object();
|
||||
}
|
||||
|
||||
PYBIND11_NAMESPACE_END(detail)
|
||||
PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE)
|
||||
|
||||
@@ -154,6 +154,7 @@ set(PYBIND11_TEST_FILES
|
||||
test_methods_and_attributes
|
||||
test_modules
|
||||
test_multiple_inheritance
|
||||
test_native_enum
|
||||
test_numpy_array
|
||||
test_numpy_dtypes
|
||||
test_numpy_vectorize
|
||||
|
||||
@@ -38,6 +38,7 @@ main_headers = {
|
||||
"include/pybind11/gil.h",
|
||||
"include/pybind11/gil_safe_call_once.h",
|
||||
"include/pybind11/iostream.h",
|
||||
"include/pybind11/native_enum.h",
|
||||
"include/pybind11/numpy.h",
|
||||
"include/pybind11/operators.h",
|
||||
"include/pybind11/options.h",
|
||||
@@ -66,6 +67,7 @@ detail_headers = {
|
||||
"include/pybind11/detail/dynamic_raw_ptr_cast_if_possible.h",
|
||||
"include/pybind11/detail/init.h",
|
||||
"include/pybind11/detail/internals.h",
|
||||
"include/pybind11/detail/native_enum_data.h",
|
||||
"include/pybind11/detail/struct_smart_holder.h",
|
||||
"include/pybind11/detail/type_caster_base.h",
|
||||
"include/pybind11/detail/typeid.h",
|
||||
|
||||
@@ -130,4 +130,20 @@ TEST_SUBMODULE(enums, m) {
|
||||
py::enum_<ScopedBoolEnum>(m, "ScopedBoolEnum")
|
||||
.value("FALSE", ScopedBoolEnum::FALSE)
|
||||
.value("TRUE", ScopedBoolEnum::TRUE);
|
||||
|
||||
#if defined(__MINGW32__)
|
||||
m.attr("obj_cast_UnscopedEnum_ptr") = "MinGW: dangling pointer to an unnamed temporary may be "
|
||||
"used [-Werror=dangling-pointer=]";
|
||||
#else
|
||||
m.def("obj_cast_UnscopedEnum_ptr", [](const py::object &obj) {
|
||||
// https://github.com/OpenImageIO/oiio/blob/30ea4ebdfab11aec291befbaff446f2a7d24835b/src/python/py_oiio.h#L300
|
||||
if (py::isinstance<UnscopedEnum>(obj)) {
|
||||
if (*obj.cast<UnscopedEnum *>() == UnscopedEnum::ETwo) {
|
||||
return 2;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -331,3 +331,12 @@ def test_generated_dunder_methods_pos_only():
|
||||
)
|
||||
is not None
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
isinstance(m.obj_cast_UnscopedEnum_ptr, str), reason=m.obj_cast_UnscopedEnum_ptr
|
||||
)
|
||||
def test_obj_cast_unscoped_enum_ptr():
|
||||
assert m.obj_cast_UnscopedEnum_ptr(m.UnscopedEnum.ETwo) == 2
|
||||
assert m.obj_cast_UnscopedEnum_ptr(m.UnscopedEnum.EOne) == 1
|
||||
assert m.obj_cast_UnscopedEnum_ptr(None) == 0
|
||||
|
||||
234
tests/test_native_enum.cpp
Normal file
234
tests/test_native_enum.cpp
Normal file
@@ -0,0 +1,234 @@
|
||||
#include <pybind11/native_enum.h>
|
||||
|
||||
#include "pybind11_tests.h"
|
||||
|
||||
#include <typeindex>
|
||||
|
||||
namespace test_native_enum {
|
||||
|
||||
// https://en.cppreference.com/w/cpp/language/enum
|
||||
|
||||
// enum that takes 16 bits
|
||||
enum smallenum : std::int16_t { a, b, c };
|
||||
|
||||
// color may be red (value 0), yellow (value 1), green (value 20), or blue (value 21)
|
||||
enum color { red, yellow, green = 20, blue };
|
||||
|
||||
// altitude may be altitude::high or altitude::low
|
||||
enum class altitude : char {
|
||||
high = 'h',
|
||||
low = 'l', // trailing comma only allowed after CWG518
|
||||
};
|
||||
|
||||
enum class flags_uchar : unsigned char { bit0 = 0x1u, bit1 = 0x2u, bit2 = 0x4u };
|
||||
enum class flags_uint : unsigned int { bit0 = 0x1u, bit1 = 0x2u, bit2 = 0x4u };
|
||||
|
||||
enum class export_values { exv0, exv1 };
|
||||
|
||||
enum class member_doc { mem0, mem1, mem2 };
|
||||
|
||||
struct class_with_enum {
|
||||
enum class in_class { one, two };
|
||||
};
|
||||
|
||||
// https://github.com/protocolbuffers/protobuf/blob/d70b5c5156858132decfdbae0a1103e6a5cb1345/src/google/protobuf/generated_enum_util.h#L52-L53
|
||||
template <typename T>
|
||||
struct is_proto_enum : std::false_type {};
|
||||
|
||||
enum some_proto_enum : int { Zero, One };
|
||||
|
||||
template <>
|
||||
struct is_proto_enum<some_proto_enum> : std::true_type {};
|
||||
|
||||
} // namespace test_native_enum
|
||||
|
||||
PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE)
|
||||
PYBIND11_NAMESPACE_BEGIN(detail)
|
||||
|
||||
template <typename ProtoEnumType>
|
||||
struct type_caster_enum_type_enabled<
|
||||
ProtoEnumType,
|
||||
detail::enable_if_t<test_native_enum::is_proto_enum<ProtoEnumType>::value>> : std::false_type {
|
||||
};
|
||||
|
||||
// https://github.com/pybind/pybind11_protobuf/blob/a50899c2eb604fc5f25deeb8901eff6231b8b3c0/pybind11_protobuf/enum_type_caster.h#L101-L105
|
||||
template <typename ProtoEnumType>
|
||||
struct type_caster<ProtoEnumType,
|
||||
detail::enable_if_t<test_native_enum::is_proto_enum<ProtoEnumType>::value>> {
|
||||
static handle
|
||||
cast(const ProtoEnumType & /*src*/, return_value_policy /*policy*/, handle /*parent*/) {
|
||||
return py::none();
|
||||
}
|
||||
|
||||
bool load(handle /*src*/, bool /*convert*/) {
|
||||
value = static_cast<ProtoEnumType>(0);
|
||||
return true;
|
||||
}
|
||||
|
||||
PYBIND11_TYPE_CASTER(ProtoEnumType, const_name<ProtoEnumType>());
|
||||
};
|
||||
|
||||
PYBIND11_NAMESPACE_END(detail)
|
||||
PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE)
|
||||
|
||||
TEST_SUBMODULE(native_enum, m) {
|
||||
using namespace test_native_enum;
|
||||
|
||||
py::native_enum<smallenum>(m, "smallenum", "enum.IntEnum")
|
||||
.value("a", smallenum::a)
|
||||
.value("b", smallenum::b)
|
||||
.value("c", smallenum::c)
|
||||
.finalize();
|
||||
|
||||
py::native_enum<color>(m, "color", "enum.IntEnum")
|
||||
.value("red", color::red)
|
||||
.value("yellow", color::yellow)
|
||||
.value("green", color::green)
|
||||
.value("blue", color::blue)
|
||||
.finalize();
|
||||
|
||||
py::native_enum<altitude>(m, "altitude")
|
||||
.value("high", altitude::high)
|
||||
.value("low", altitude::low)
|
||||
.finalize();
|
||||
|
||||
py::native_enum<flags_uchar>(m, "flags_uchar", "enum.Flag")
|
||||
.value("bit0", flags_uchar::bit0)
|
||||
.value("bit1", flags_uchar::bit1)
|
||||
.value("bit2", flags_uchar::bit2)
|
||||
.finalize();
|
||||
|
||||
py::native_enum<flags_uint>(m, "flags_uint", "enum.IntFlag")
|
||||
.value("bit0", flags_uint::bit0)
|
||||
.value("bit1", flags_uint::bit1)
|
||||
.value("bit2", flags_uint::bit2)
|
||||
.finalize();
|
||||
|
||||
py::native_enum<export_values>(m, "export_values", "enum.IntEnum")
|
||||
.value("exv0", export_values::exv0)
|
||||
.value("exv1", export_values::exv1)
|
||||
.export_values()
|
||||
.finalize();
|
||||
|
||||
py::native_enum<member_doc>(m, "member_doc", "enum.IntEnum")
|
||||
.value("mem0", member_doc::mem0, "docA")
|
||||
.value("mem1", member_doc::mem1)
|
||||
.value("mem2", member_doc::mem2, "docC")
|
||||
.finalize();
|
||||
|
||||
py::class_<class_with_enum> py_class_with_enum(m, "class_with_enum");
|
||||
py::native_enum<class_with_enum::in_class>(py_class_with_enum, "in_class", "enum.IntEnum")
|
||||
.value("one", class_with_enum::in_class::one)
|
||||
.value("two", class_with_enum::in_class::two)
|
||||
.finalize();
|
||||
|
||||
m.def("isinstance_color", [](const py::object &obj) { return py::isinstance<color>(obj); });
|
||||
|
||||
m.def("pass_color", [](color e) { return static_cast<int>(e); });
|
||||
m.def("return_color", [](int i) { return static_cast<color>(i); });
|
||||
|
||||
m.def("pass_some_proto_enum", [](some_proto_enum) { return py::none(); });
|
||||
m.def("return_some_proto_enum", []() { return some_proto_enum::Zero; });
|
||||
|
||||
#if defined(__MINGW32__)
|
||||
m.attr("obj_cast_color_ptr") = "MinGW: dangling pointer to an unnamed temporary may be used "
|
||||
"[-Werror=dangling-pointer=]";
|
||||
#elif defined(NDEBUG)
|
||||
m.attr("obj_cast_color_ptr") = "NDEBUG disables cast safety check";
|
||||
#else
|
||||
m.def("obj_cast_color_ptr", [](const py::object &obj) { obj.cast<color *>(); });
|
||||
#endif
|
||||
|
||||
m.def("py_cast_color_handle", [](py::handle obj) {
|
||||
// Exercises `if (is_enum_cast && cast_is_temporary_value_reference<T>::value)`
|
||||
// in `T cast(const handle &handle)`
|
||||
auto e = py::cast<color>(obj);
|
||||
return static_cast<int>(e);
|
||||
});
|
||||
|
||||
m.def("exercise_import_or_getattr", [](py::module_ &m, const char *native_type_name) {
|
||||
enum fake { x };
|
||||
py::native_enum<fake>(m, "fake_import_or_getattr", native_type_name)
|
||||
.value("x", fake::x)
|
||||
.finalize();
|
||||
});
|
||||
|
||||
m.def("native_enum_data_missing_finalize_error_message",
|
||||
[](const std::string &enum_name_encoded) {
|
||||
return py::detail::native_enum_missing_finalize_error_message(enum_name_encoded);
|
||||
});
|
||||
|
||||
m.def("native_enum_ctor_malformed_utf8", [](const char *malformed_utf8) {
|
||||
enum fake { x };
|
||||
py::native_enum<fake>{py::none(), malformed_utf8, "enum.IntEnum"};
|
||||
});
|
||||
|
||||
m.def("native_enum_double_finalize", [](py::module_ &m) {
|
||||
enum fake { x };
|
||||
py::native_enum<fake> ne(m, "fake_native_enum_double_finalize", "enum.IntEnum");
|
||||
ne.finalize();
|
||||
ne.finalize();
|
||||
});
|
||||
|
||||
m.def("native_enum_value_after_finalize", [](py::module_ &m) {
|
||||
enum fake { x };
|
||||
py::native_enum<fake> ne(m, "fake_native_enum_value_after_finalize", "enum.IntEnum");
|
||||
ne.finalize();
|
||||
ne.value("x", fake::x);
|
||||
});
|
||||
|
||||
m.def("native_enum_value_malformed_utf8", [](const char *malformed_utf8) {
|
||||
enum fake { x };
|
||||
py::native_enum<fake>(py::none(), "fake", "enum.IntEnum").value(malformed_utf8, fake::x);
|
||||
});
|
||||
|
||||
m.def("double_registration_native_enum", [](py::module_ &m) {
|
||||
enum fake { x };
|
||||
py::native_enum<fake>(m, "fake_double_registration_native_enum", "enum.IntEnum")
|
||||
.value("x", fake::x)
|
||||
.finalize();
|
||||
py::native_enum<fake>(m, "fake_double_registration_native_enum");
|
||||
});
|
||||
|
||||
m.def("native_enum_name_clash", [](py::module_ &m) {
|
||||
enum fake { x };
|
||||
py::native_enum<fake>(m, "fake_native_enum_name_clash", "enum.IntEnum")
|
||||
.value("x", fake::x)
|
||||
.finalize();
|
||||
});
|
||||
|
||||
m.def("native_enum_value_name_clash", [](py::module_ &m) {
|
||||
enum fake { x };
|
||||
py::native_enum<fake>(m, "fake_native_enum_value_name_clash", "enum.IntEnum")
|
||||
.value("fake_native_enum_value_name_clash_x", fake::x)
|
||||
.export_values()
|
||||
.finalize();
|
||||
});
|
||||
|
||||
m.def("double_registration_enum_before_native_enum", [](py::module_ &m) {
|
||||
enum fake { x };
|
||||
py::enum_<fake>(m, "fake_enum_first").value("x", fake::x);
|
||||
py::native_enum<fake>(m, "fake_enum_first", "enum.IntEnum").value("x", fake::x).finalize();
|
||||
});
|
||||
|
||||
m.def("double_registration_native_enum_before_enum", [](py::module_ &m) {
|
||||
enum fake { x };
|
||||
py::native_enum<fake>(m, "fake_native_enum_first", "enum.IntEnum")
|
||||
.value("x", fake::x)
|
||||
.finalize();
|
||||
py::enum_<fake>(m, "name_must_be_different_to_reach_desired_code_path");
|
||||
});
|
||||
|
||||
#if defined(PYBIND11_NEGATE_THIS_CONDITION_FOR_LOCAL_TESTING) && !defined(NDEBUG)
|
||||
m.def("native_enum_missing_finalize_failure", []() {
|
||||
enum fake { x };
|
||||
py::native_enum<fake>(
|
||||
py::none(), "fake_native_enum_missing_finalize_failure", "enum.IntEnum")
|
||||
.value("x", fake::x)
|
||||
// .finalize() missing
|
||||
;
|
||||
});
|
||||
#else
|
||||
m.attr("native_enum_missing_finalize_failure") = "For local testing only: terminates process";
|
||||
#endif
|
||||
}
|
||||
297
tests/test_native_enum.py
Normal file
297
tests/test_native_enum.py
Normal file
@@ -0,0 +1,297 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import enum
|
||||
import pickle
|
||||
|
||||
import pytest
|
||||
|
||||
import env
|
||||
from pybind11_tests import native_enum as m
|
||||
|
||||
SMALLENUM_MEMBERS = (
|
||||
("a", 0),
|
||||
("b", 1),
|
||||
("c", 2),
|
||||
)
|
||||
|
||||
COLOR_MEMBERS = (
|
||||
("red", 0),
|
||||
("yellow", 1),
|
||||
("green", 20),
|
||||
("blue", 21),
|
||||
)
|
||||
|
||||
ALTITUDE_MEMBERS = (
|
||||
("high", "h"),
|
||||
("low", "l"),
|
||||
)
|
||||
|
||||
FLAGS_UCHAR_MEMBERS = (
|
||||
("bit0", 0x1),
|
||||
("bit1", 0x2),
|
||||
("bit2", 0x4),
|
||||
)
|
||||
|
||||
FLAGS_UINT_MEMBERS = (
|
||||
("bit0", 0x1),
|
||||
("bit1", 0x2),
|
||||
("bit2", 0x4),
|
||||
)
|
||||
|
||||
CLASS_WITH_ENUM_IN_CLASS_MEMBERS = (
|
||||
("one", 0),
|
||||
("two", 1),
|
||||
)
|
||||
|
||||
EXPORT_VALUES_MEMBERS = (
|
||||
("exv0", 0),
|
||||
("exv1", 1),
|
||||
)
|
||||
|
||||
MEMBER_DOC_MEMBERS = (
|
||||
("mem0", 0),
|
||||
("mem1", 1),
|
||||
("mem2", 2),
|
||||
)
|
||||
|
||||
ENUM_TYPES_AND_MEMBERS = (
|
||||
(m.smallenum, SMALLENUM_MEMBERS),
|
||||
(m.color, COLOR_MEMBERS),
|
||||
(m.altitude, ALTITUDE_MEMBERS),
|
||||
(m.flags_uchar, FLAGS_UCHAR_MEMBERS),
|
||||
(m.flags_uint, FLAGS_UINT_MEMBERS),
|
||||
(m.export_values, EXPORT_VALUES_MEMBERS),
|
||||
(m.member_doc, MEMBER_DOC_MEMBERS),
|
||||
(m.class_with_enum.in_class, CLASS_WITH_ENUM_IN_CLASS_MEMBERS),
|
||||
)
|
||||
|
||||
ENUM_TYPES = [_[0] for _ in ENUM_TYPES_AND_MEMBERS]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("enum_type", ENUM_TYPES)
|
||||
def test_enum_type(enum_type):
|
||||
assert isinstance(enum_type, enum.EnumMeta)
|
||||
assert enum_type.__module__ == m.__name__
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("enum_type", "members"), ENUM_TYPES_AND_MEMBERS)
|
||||
def test_enum_members(enum_type, members):
|
||||
for name, value in members:
|
||||
assert enum_type[name].value == value
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("enum_type", "members"), ENUM_TYPES_AND_MEMBERS)
|
||||
def test_pickle_roundtrip(enum_type, members):
|
||||
for name, _ in members:
|
||||
orig = enum_type[name]
|
||||
if enum_type is m.class_with_enum.in_class:
|
||||
# This is a general pickle limitation.
|
||||
with pytest.raises(pickle.PicklingError):
|
||||
pickle.dumps(orig)
|
||||
else:
|
||||
# This only works if __module__ is correct.
|
||||
serialized = pickle.dumps(orig)
|
||||
restored = pickle.loads(serialized)
|
||||
assert restored == orig
|
||||
|
||||
|
||||
@pytest.mark.parametrize("enum_type", [m.flags_uchar, m.flags_uint])
|
||||
def test_enum_flag(enum_type):
|
||||
bits02 = enum_type.bit0 | enum_type.bit2
|
||||
assert enum_type.bit0 in bits02
|
||||
assert enum_type.bit1 not in bits02
|
||||
assert enum_type.bit2 in bits02
|
||||
|
||||
|
||||
def test_export_values():
|
||||
assert m.exv0 is m.export_values.exv0
|
||||
assert m.exv1 is m.export_values.exv1
|
||||
|
||||
|
||||
def test_member_doc():
|
||||
pure_native = enum.IntEnum("pure_native", (("mem", 0),))
|
||||
assert m.member_doc.mem0.__doc__ == "docA"
|
||||
assert m.member_doc.mem1.__doc__ == pure_native.mem.__doc__
|
||||
assert m.member_doc.mem2.__doc__ == "docC"
|
||||
|
||||
|
||||
def test_pybind11_isinstance_color():
|
||||
for name, _ in COLOR_MEMBERS:
|
||||
assert m.isinstance_color(m.color[name])
|
||||
assert not m.isinstance_color(m.color)
|
||||
for name, _ in SMALLENUM_MEMBERS:
|
||||
assert not m.isinstance_color(m.smallenum[name])
|
||||
assert not m.isinstance_color(m.smallenum)
|
||||
assert not m.isinstance_color(None)
|
||||
|
||||
|
||||
def test_pass_color_success():
|
||||
for name, value in COLOR_MEMBERS:
|
||||
assert m.pass_color(m.color[name]) == value
|
||||
|
||||
|
||||
def test_pass_color_fail():
|
||||
with pytest.raises(TypeError) as excinfo:
|
||||
m.pass_color(None)
|
||||
assert "test_native_enum::color" in str(excinfo.value)
|
||||
|
||||
|
||||
def test_return_color_success():
|
||||
for name, value in COLOR_MEMBERS:
|
||||
assert m.return_color(value) == m.color[name]
|
||||
|
||||
|
||||
def test_return_color_fail():
|
||||
with pytest.raises(ValueError) as excinfo_direct:
|
||||
m.color(2)
|
||||
with pytest.raises(ValueError) as excinfo_cast:
|
||||
m.return_color(2)
|
||||
assert str(excinfo_cast.value) == str(excinfo_direct.value)
|
||||
|
||||
|
||||
def test_type_caster_enum_type_enabled_false():
|
||||
# This is really only a "does it compile" test.
|
||||
assert m.pass_some_proto_enum(None) is None
|
||||
assert m.return_some_proto_enum() is None
|
||||
|
||||
|
||||
@pytest.mark.skipif(isinstance(m.obj_cast_color_ptr, str), reason=m.obj_cast_color_ptr)
|
||||
def test_obj_cast_color_ptr():
|
||||
with pytest.raises(RuntimeError) as excinfo:
|
||||
m.obj_cast_color_ptr(m.color.red)
|
||||
assert str(excinfo.value) == "Unable to cast native enum type to reference"
|
||||
|
||||
|
||||
def test_py_cast_color_handle():
|
||||
for name, value in COLOR_MEMBERS:
|
||||
assert m.py_cast_color_handle(m.color[name]) == value
|
||||
|
||||
|
||||
def test_exercise_import_or_getattr_leading_dot():
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
m.exercise_import_or_getattr(m, ".")
|
||||
assert str(excinfo.value) == "Invalid fully-qualified name `.` (native_type_name)"
|
||||
|
||||
|
||||
def test_exercise_import_or_getattr_bad_top_level():
|
||||
with pytest.raises(ImportError) as excinfo:
|
||||
m.exercise_import_or_getattr(m, "NeVeRLaNd")
|
||||
assert (
|
||||
str(excinfo.value)
|
||||
== "Failed to import top-level module `NeVeRLaNd` (native_type_name)"
|
||||
)
|
||||
|
||||
|
||||
def test_exercise_import_or_getattr_dot_dot():
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
m.exercise_import_or_getattr(m, "enum..")
|
||||
assert (
|
||||
str(excinfo.value) == "Invalid fully-qualified name `enum..` (native_type_name)"
|
||||
)
|
||||
|
||||
|
||||
def test_exercise_import_or_getattr_bad_enum_attr():
|
||||
with pytest.raises(ImportError) as excinfo:
|
||||
m.exercise_import_or_getattr(m, "enum.NoNeXiStInG")
|
||||
lines = str(excinfo.value).splitlines()
|
||||
assert len(lines) >= 5
|
||||
assert (
|
||||
lines[0]
|
||||
== "Failed to import or getattr `NoNeXiStInG` from `enum` (native_type_name)"
|
||||
)
|
||||
assert lines[1] == "-------- getattr exception --------"
|
||||
ix = lines.index("-------- import exception --------")
|
||||
assert ix >= 3
|
||||
assert len(lines) > ix + 0
|
||||
|
||||
|
||||
def test_native_enum_data_missing_finalize_error_message():
|
||||
msg = m.native_enum_data_missing_finalize_error_message("Fake")
|
||||
assert msg == 'pybind11::native_enum<...>("Fake", ...): MISSING .finalize()'
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"func", [m.native_enum_ctor_malformed_utf8, m.native_enum_value_malformed_utf8]
|
||||
)
|
||||
def test_native_enum_malformed_utf8(func):
|
||||
if env.GRAALPY and func is m.native_enum_ctor_malformed_utf8:
|
||||
pytest.skip("GraalPy does not raise UnicodeDecodeError")
|
||||
malformed_utf8 = b"\x80"
|
||||
with pytest.raises(UnicodeDecodeError):
|
||||
func(malformed_utf8)
|
||||
|
||||
|
||||
def test_native_enum_double_finalize():
|
||||
with pytest.raises(RuntimeError) as excinfo:
|
||||
m.native_enum_double_finalize(m)
|
||||
assert (
|
||||
str(excinfo.value)
|
||||
== 'pybind11::native_enum<...>("fake_native_enum_double_finalize"): DOUBLE finalize'
|
||||
)
|
||||
|
||||
|
||||
def test_native_enum_value_after_finalize():
|
||||
with pytest.raises(RuntimeError) as excinfo:
|
||||
m.native_enum_value_after_finalize(m)
|
||||
assert (
|
||||
str(excinfo.value)
|
||||
== 'pybind11::native_enum<...>("fake_native_enum_value_after_finalize"): value after finalize'
|
||||
)
|
||||
|
||||
|
||||
def test_double_registration_native_enum():
|
||||
with pytest.raises(RuntimeError) as excinfo:
|
||||
m.double_registration_native_enum(m)
|
||||
assert (
|
||||
str(excinfo.value)
|
||||
== 'pybind11::native_enum<...>("fake_double_registration_native_enum") is already registered!'
|
||||
)
|
||||
|
||||
|
||||
def test_native_enum_name_clash():
|
||||
m.fake_native_enum_name_clash = None
|
||||
with pytest.raises(RuntimeError) as excinfo:
|
||||
m.native_enum_name_clash(m)
|
||||
assert (
|
||||
str(excinfo.value)
|
||||
== 'pybind11::native_enum<...>("fake_native_enum_name_clash"):'
|
||||
" an object with that name is already defined"
|
||||
)
|
||||
|
||||
|
||||
def test_native_enum_value_name_clash():
|
||||
m.fake_native_enum_value_name_clash_x = None
|
||||
with pytest.raises(RuntimeError) as excinfo:
|
||||
m.native_enum_value_name_clash(m)
|
||||
assert (
|
||||
str(excinfo.value)
|
||||
== 'pybind11::native_enum<...>("fake_native_enum_value_name_clash")'
|
||||
'.value("fake_native_enum_value_name_clash_x"):'
|
||||
" an object with that name is already defined"
|
||||
)
|
||||
|
||||
|
||||
def test_double_registration_enum_before_native_enum():
|
||||
with pytest.raises(RuntimeError) as excinfo:
|
||||
m.double_registration_enum_before_native_enum(m)
|
||||
assert (
|
||||
str(excinfo.value)
|
||||
== 'pybind11::native_enum<...>("fake_enum_first") is already registered'
|
||||
" as a `pybind11::enum_` or `pybind11::class_`!"
|
||||
)
|
||||
|
||||
|
||||
def test_double_registration_native_enum_before_enum():
|
||||
with pytest.raises(RuntimeError) as excinfo:
|
||||
m.double_registration_native_enum_before_enum(m)
|
||||
assert (
|
||||
str(excinfo.value)
|
||||
== 'pybind11::enum_ "name_must_be_different_to_reach_desired_code_path"'
|
||||
" is already registered as a pybind11::native_enum!"
|
||||
)
|
||||
|
||||
|
||||
def test_native_enum_missing_finalize_failure():
|
||||
if not isinstance(m.native_enum_missing_finalize_failure, str):
|
||||
m.native_enum_missing_finalize_failure()
|
||||
pytest.fail("Process termination expected.")
|
||||
Reference in New Issue
Block a user