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:
Ralf W. Grosse-Kunstleve
2025-03-24 20:31:59 -07:00
committed by GitHub
parent 48eb5ad9b9
commit f365314ec0
18 changed files with 1158 additions and 58 deletions

View File

@@ -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

View File

@@ -1,3 +1,5 @@
.. _custom_type_caster:
Custom type casters
===================

View 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).

View File

@@ -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.

View File

@@ -36,6 +36,7 @@
advanced/pycpp/index
advanced/embedding
advanced/misc
advanced/deprecated
.. toctree::
:caption: Extra Information

View File

@@ -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));
}

View File

@@ -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 :

View File

@@ -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;

View 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)

View 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)

View File

@@ -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);

View File

@@ -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)

View File

@@ -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

View File

@@ -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",

View File

@@ -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
}

View File

@@ -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
View 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
View 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.")