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 2 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
5 changes: 5 additions & 0 deletions docs/classes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -560,3 +560,8 @@ The ``name`` property returns the name of the enum value as a unicode string.
.. 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.


.. note::

``py::native_enum`` was added as an alternative to ``py::enum_``
with http://github.com/pybind/pybind11/pull/5555
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
123 changes: 123 additions & 0 deletions include/pybind11/detail/native_enum_data.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// 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 <string>
#include <typeindex>

PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE)
PYBIND11_NAMESPACE_BEGIN(detail)

class native_enum_data {
public:
native_enum_data(const char *enum_name,
const std::type_index &enum_type_index,
bool use_int_enum)
: enum_name_encoded{enum_name}, enum_type_index{enum_type_index},
use_int_enum{use_int_enum}, enum_name{enum_name} {}

native_enum_data(const native_enum_data &) = delete;
native_enum_data &operator=(const native_enum_data &) = delete;

void disarm_correct_use_check() const { correct_use_check = false; }
void arm_correct_use_check() const { correct_use_check = true; }

// This is a separate public function only to enable easy unit testing.
std::string was_not_added_error_message() const {
return "`native_enum` was not added to any module."
" Use e.g. `m += native_enum<...>(\""
+ enum_name_encoded + "\", ...)` to fix.";
}

#if !defined(NDEBUG)
// This dtor cannot easily be unit tested because it terminates the process.
~native_enum_data() {
if (correct_use_check) {
pybind11_fail(was_not_added_error_message());
}
}
#endif

private:
mutable bool correct_use_check{false};

public:
std::string enum_name_encoded;
std::type_index enum_type_index;
bool use_int_enum;
bool export_values_flag{false};
str enum_name;
list members;
list docs;
};

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 void native_enum_add_to_parent(const object &parent, const detail::native_enum_data &data) {
data.disarm_correct_use_check();
if (hasattr(parent, data.enum_name)) {
pybind11_fail("pybind11::native_enum<...>(\"" + data.enum_name_encoded
+ "\"): an object with that name is already defined");
}
auto enum_module = reinterpret_steal<object>(PyImport_ImportModule("enum"));
if (!enum_module) {
raise_from(PyExc_SystemError,
"`import enum` FAILED at " __FILE__ ":" PYBIND11_TOSTRING(__LINE__));
throw error_already_set();
}
auto py_enum_type = enum_module.attr(data.use_int_enum ? "IntEnum" : "Enum");
auto py_enum = py_enum_type(data.enum_name, data.members);
object module_name = get_module_name_if_available(parent);
if (module_name) {
py_enum.attr("__module__") = module_name;
}
parent.attr(data.enum_name) = py_enum;
if (data.export_values_flag) {
for (auto member : data.members) {
auto member_name = member[int_(0)];
if (hasattr(parent, member_name)) {
pybind11_fail("pybind11::native_enum<...>(\"" + data.enum_name_encoded
+ "\").value(\"" + member_name.cast<std::string>()
+ "\"): an object with that name is already defined");
}
parent.attr(member_name) = py_enum[member_name];
}
}
for (auto doc : data.docs) {
py_enum[doc[int_(0)]].attr("__doc__") = doc[int_(1)];
}
global_internals_native_enum_type_map_set_item(data.enum_type_index, py_enum.release().ptr());
}

PYBIND11_NAMESPACE_END(detail)
PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE)
63 changes: 63 additions & 0 deletions include/pybind11/native_enum.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// 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 <limits>
#include <type_traits>
#include <typeindex>

PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE)

enum class native_enum_kind { Enum, IntEnum };

/// Conversions between Python's native (stdlib) enum types and C++ enums.
template <typename Type>
class native_enum : public detail::native_enum_data {
public:
using Underlying = typename std::underlying_type<Type>::type;

explicit native_enum(const char *name, native_enum_kind kind)
: detail::native_enum_data(
name, std::type_index(typeid(Type)), kind == native_enum_kind::IntEnum) {
if (detail::get_local_type_info(typeid(Type)) != nullptr
|| detail::get_global_type_info(typeid(Type)) != 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_correct_use_check();
}

/// Export enumeration entries into the parent scope
native_enum &export_values() {
export_values_flag = true;
return *this;
}

/// Add an enumeration entry
native_enum &value(char const *name, Type value, const char *doc = nullptr) {
disarm_correct_use_check();
members.append(make_tuple(name, static_cast<Underlying>(value)));
if (doc) {
docs.append(make_tuple(name, doc));
}
arm_correct_use_check();
return *this;
}

native_enum(const native_enum &) = delete;
native_enum &operator=(const native_enum &) = delete;
};

PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE)
Loading
Loading