Skip to content

feat: enable conversions between native Python enum types and C++ enums #5555

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 32 commits into from
Mar 25, 2025
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
80d9936
Apply smart_holder-branch-based PR #5280 on top of master.
rwgk Mar 8, 2025
06eddc7
Add pytest.skip("GraalPy does not raise UnicodeDecodeError")
rwgk Mar 8, 2025
ce5859e
Add `parent_scope` as first argument to `py::native_enum` ctor.
rwgk Mar 9, 2025
943d066
Replace `operator+=` API with `.finalize()` API. The error messages s…
rwgk Mar 9, 2025
7689e5b
Resolve clang-tidy performance-unnecessary-value-param errors
rwgk Mar 9, 2025
1a42257
Rename (effectively) native_enum_add_to_parent() -> finalize()
rwgk Mar 9, 2025
bab3348
Update error message: pybind11::native_enum<...>("Fake", ...): MISSIN…
rwgk Mar 9, 2025
c22104f
Pass py::module_ by reference to resolve clang-tidy errors (this is e…
rwgk Mar 9, 2025
0da489d
test_native_enum_correct_use_failure -> test_native_enum_missing_fina…
rwgk Mar 9, 2025
fb10d3b
Add test_native_enum_double_finalize(), test_native_enum_value_after_…
rwgk Mar 10, 2025
7acb18a
Clean up public/protected API.
rwgk Mar 10, 2025
8b07a74
[ci skip] Update the Enumerations section in classes.rst
rwgk Mar 10, 2025
4be8393
Merge branch 'master' into native_enum_master
rwgk Mar 15, 2025
11f597b
Rename `py::native_enum_kind` → `py::enum_kind` as suggested by gh-he…
rwgk Mar 15, 2025
190f99c
Experiment: StrEnum
rwgk Mar 15, 2025
df39090
Remove StrEnum code.
rwgk Mar 15, 2025
6180ebc
Make enum_kind::Enum the default kind.
rwgk Mar 16, 2025
a0218f4
Catch redundant .export_values() calls.
rwgk Mar 16, 2025
32e848a
[ci skip] Add back original documentation for `py::enum_` under new a…
rwgk Mar 16, 2025
ff25f5a
[ci skip] Add documentation for `py::enum_kind` and `py::detail::type…
rwgk Mar 16, 2025
1c13866
Rename `Type` to `EnumType` for readability.
rwgk Mar 16, 2025
8efb9bf
Eliminate py::enum_kind, use "enum.Enum", "enum.IntEnum" directly. Th…
rwgk Mar 16, 2025
9918c09
Merge branch 'master' into native_enum_master
rwgk Mar 22, 2025
9483e8c
EXPERIMENTAL StrEnum code. To be removed.
rwgk Mar 22, 2025
0e46fa0
Remove experimental StrEnum code:
rwgk Mar 22, 2025
346b47f
Add test with enum.IntFlag (no production code changes required).
rwgk Mar 22, 2025
050febc
First import_or_getattr() implementation (dedicated tests are still m…
rwgk Mar 22, 2025
f773626
Fix import_or_getattr() implementation, add tests, fix clang-tidy err…
rwgk Mar 23, 2025
29f143a
[ci skip] Update classes.rst: replace `py::enum_kind` with `native_ty…
rwgk Mar 23, 2025
f885205
Merge branch 'master' into native_enum_master
rwgk Mar 24, 2025
cbb7494
For "constructor similar to that of enum.Enum" point to https://docs.…
rwgk Mar 24, 2025
ae07bd2
Advertise Enum, IntEnum, Flag, IntFlags are compatible stdlib enum ty…
rwgk Mar 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,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
Expand All @@ -160,6 +161,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
Expand Down
88 changes: 28 additions & 60 deletions docs/classes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -459,8 +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.

Enumerations and internal types
===============================
Enumerations
============

Let's now suppose that the example class contains internal types like enumerations, e.g.:

Expand Down Expand Up @@ -494,69 +494,37 @@ 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.

.. 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).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm. Is this warning captured anywhere else? It doesn't appear to be, but if we're keeping the old enum stuff it probably should be somewhere? Maybe this (and other things that were removed) could be added in a separate note?

.. note:: The deprecated `py::enum_` differs from standard python enums in the following ways:  ... 

Copy link
Collaborator Author

@rwgk rwgk Mar 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

The ``.finalize()`` call above is needed because Python's native enums
cannot be built incrementally, but 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.

``py::native_enum`` was introduced with pybind11 release v3.0.0. It binds C++
enum types to Python's native enum.Enum, making them PEP 435 compatible. 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``.

For details about the deprecated ``py::enum_``, please refer to
:file:`tests/test_enum.cpp` and
:file:`tests/test_enum.py`.
110 changes: 109 additions & 1 deletion include/pybind11/cast.h
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -1468,8 +1567,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));
}

Expand Down
10 changes: 1 addition & 9 deletions include/pybind11/detail/class.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 :
Expand Down
8 changes: 5 additions & 3 deletions include/pybind11/detail/internals.h
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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;
Expand Down
Loading