Skip to content

Commit f8128f0

Browse files
authored
Conversions between Python's native (stdlib) enum types and C++ enums. (#30005)
* Transfer PR #4329 from master to smart_holder branch, STEP 1. The patch .rej below are resolved, but THIS STATE DOES NOT BUILD: ``` clang++ -o pybind11/tests/test_constants_and_functions.os -c -std=c++17 -fPIC -fvisibility=hidden -O0 -g -Wall -Wextra -Wconversion -Wcast-qual -Wdeprecated -Wundef -Wnon-virtual-dtor -Wunused-result -Werror -isystem /usr/include/python3.10 -isystem /usr/include/eigen3 -DPYBIND11_STRICT_ASSERTS_CLASS_HOLDER_VS_TYPE_CASTER_MIX -DPYBIND11_ENABLE_TYPE_CASTER_ODR_GUARD_IF_AVAILABLE -DPYBIND11_TEST_BOOST -Ipybind11/include -I/usr/local/google/home/rwgk/forked/pybind11/include -I/usr/local/google/home/rwgk/clone/pybind11/include /usr/local/google/home/rwgk/forked/pybind11/tests/test_constants_and_functions.cpp In file included from /usr/local/google/home/rwgk/forked/pybind11/tests/test_constants_and_functions.cpp:11: In file included from /usr/local/google/home/rwgk/forked/pybind11/tests/pybind11_tests.h:3: In file included from /usr/local/google/home/rwgk/forked/pybind11/include/pybind11/eval.h:14: /usr/local/google/home/rwgk/forked/pybind11/include/pybind11/pybind11.h:1781:9: error: static_assert failed due to requirement '!holder_is_smart_holder == type_caster_type_is_type_caster_base_subtype' "py::class_ holder vs type_caster mismatch: missing PYBIND11_TYPE_CASTER_BASE_HOLDER(T, ...) or collision with custom py::detail::type_caster<T>?" static_assert(!holder_is_smart_holder == type_caster_type_is_type_caster_base_subtype, ^ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /usr/local/google/home/rwgk/forked/pybind11/include/pybind11/pybind11.h:2460:11: note: in instantiation of function template specialization 'pybind11::class_<MyEnum>::class_<>' requested here : class_<Type>(scope, name, extra...), m_base(*this, scope) { ^ /usr/local/google/home/rwgk/forked/pybind11/tests/test_constants_and_functions.cpp:106:5: note: in instantiation of function template specialization 'pybind11::enum_<MyEnum>::enum_<>' requested here py::enum_<MyEnum>(m, "MyEnum") ^ 1 error generated. ``` ________ ``` rwgk.c.googlers.com:~/forked/pybind11 $ patch -p 1 < ~/native_enum_git_diff_master_2022-11-19+131942.patch patching file .github/workflows/python312.yml patching file CMakeLists.txt Hunk #1 FAILED at 111. Hunk #2 succeeded at 138 (offset 5 lines). 1 out of 2 hunks FAILED -- saving rejects to file CMakeLists.txt.rej patching file include/pybind11/cast.h Hunk #1 FAILED at 12. Hunk #2 succeeded at 78 (offset 30 lines). Hunk #3 succeeded at 1173 (offset 41 lines). 1 out of 3 hunks FAILED -- saving rejects to file include/pybind11/cast.h.rej patching file include/pybind11/detail/abi_platform_id.h patching file include/pybind11/detail/cross_extension_shared_state.h patching file include/pybind11/detail/internals.h Hunk #1 FAILED at 16. Hunk #2 succeeded at 109 (offset 1 line). Hunk #3 FAILED at 203. Hunk #4 succeeded at 398 (offset 11 lines). Hunk #5 succeeded at 457 (offset 11 lines). 2 out of 5 hunks FAILED -- saving rejects to file include/pybind11/detail/internals.h.rej patching file include/pybind11/detail/native_enum_data.h patching file include/pybind11/detail/type_map.h patching file include/pybind11/embed.h patching file include/pybind11/native_enum.h patching file include/pybind11/pybind11.h Hunk #1 FAILED at 12. Hunk #2 succeeded at 1269 (offset 1 line). Hunk #3 succeeded at 2457 (offset 255 lines). 1 out of 3 hunks FAILED -- saving rejects to file include/pybind11/pybind11.h.rej patching file include/pybind11/pytypes.h patching file tests/CMakeLists.txt Hunk #1 succeeded at 160 (offset 18 lines). patching file tests/conftest.py patching file tests/extra_python_package/test_files.py Hunk #2 FAILED at 47. 1 out of 2 hunks FAILED -- saving rejects to file tests/extra_python_package/test_files.py.rej patching file tests/test_embed/test_interpreter.cpp patching file tests/test_enum.cpp patching file tests/test_enum.py patching file tests/test_native_enum.cpp patching file tests/test_native_enum.py ``` * Make `smart_holder` code compatible with `type_caster_enum_type` * Fix `if` condition guarding `Unable to cast native enum type to reference` * WIP native_enum_add_to_parent * PYBIND11_SILENCE_MSVC_C4127 * Transfer upstream.yml/python312.yml changes from PR #4397 * clang-tidy (clang 15) auto-fix * Remove python312.yml: this is handled separately under PR #30006 * Fixes for ruff * Replace `PyEval_GetBuiltins()` in `finalize_interpreter()` with `get_python_state_dict()` * Bug fix: Ensure `state_dict` is destroyed before `Py_Finalize()` * Restore tests/test_embed/test_interpreter.cpp from google_pywrapcc_main * Restore include/pybind11/embed.h from google_pywrapcc_main * Undo unrelated one-line change (from `auto` to `internals`). * Add missing `test_class_with_enum` * Add note to docs/classes.rst pointing to PR #30005
1 parent 92741e7 commit f8128f0

15 files changed

+804
-17
lines changed

CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ set(PYBIND11_HEADERS
119119
include/pybind11/detail/dynamic_raw_ptr_cast_if_possible.h
120120
include/pybind11/detail/init.h
121121
include/pybind11/detail/internals.h
122+
include/pybind11/detail/native_enum_data.h
122123
include/pybind11/detail/smart_holder_poc.h
123124
include/pybind11/detail/smart_holder_sfinae_hooks_only.h
124125
include/pybind11/detail/smart_holder_type_casters.h
@@ -141,6 +142,7 @@ set(PYBIND11_HEADERS
141142
include/pybind11/gil.h
142143
include/pybind11/iostream.h
143144
include/pybind11/functional.h
145+
include/pybind11/native_enum.h
144146
include/pybind11/numpy.h
145147
include/pybind11/operators.h
146148
include/pybind11/pybind11.h

docs/classes.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -539,3 +539,8 @@ The ``name`` property returns the name of the enum value as a unicode string.
539539
...
540540
541541
By default, these are omitted to conserve space.
542+
543+
.. note::
544+
545+
``py::native_enum`` was added as an alternative to ``py::enum_``
546+
with http://github.com/google/pywrapcc/pull/30005

include/pybind11/cast.h

Lines changed: 137 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
#include "detail/common.h"
1414
#include "detail/descr.h"
15+
#include "detail/native_enum_data.h"
1516
#include "detail/smart_holder_sfinae_hooks_only.h"
1617
#include "detail/type_caster_base.h"
1718
#include "detail/type_caster_odr_guard.h"
@@ -63,12 +64,6 @@ using make_caster_for_intrinsic = type_caster<type>;
6364
template <typename type>
6465
using make_caster = make_caster_for_intrinsic<intrinsic_t<type>>;
6566

66-
template <typename T>
67-
struct type_uses_smart_holder_type_caster {
68-
static constexpr bool value
69-
= std::is_base_of<smart_holder_type_caster_base_tag, make_caster<T>>::value;
70-
};
71-
7267
// Shortcut for calling a caster's `cast_op_type` cast operator for casting a type_caster to a T
7368
template <typename T>
7469
typename make_caster<T>::template cast_op_type<T> cast_op(make_caster<T> &caster) {
@@ -81,6 +76,126 @@ cast_op(make_caster<T> &&caster) {
8176
template cast_op_type<typename std::add_rvalue_reference<T>::type>();
8277
}
8378

79+
template <typename EnumType>
80+
class type_caster_enum_type {
81+
private:
82+
using Underlying = typename std::underlying_type<EnumType>::type;
83+
84+
public:
85+
static constexpr auto name = const_name<EnumType>();
86+
87+
template <typename SrcType>
88+
static handle cast(SrcType &&src, return_value_policy, handle parent) {
89+
auto const &natives = cross_extension_shared_states::native_enum_type_map::get();
90+
auto found = natives.find(std::type_index(typeid(EnumType)));
91+
if (found != natives.end()) {
92+
return handle(found->second)(static_cast<Underlying>(src)).release();
93+
}
94+
return type_caster_for_class_<EnumType>::cast(
95+
std::forward<SrcType>(src),
96+
// Fixes https://github.com/pybind/pybind11/pull/3643#issuecomment-1022987818:
97+
return_value_policy::copy,
98+
parent);
99+
}
100+
101+
bool load(handle src, bool convert) {
102+
auto const &natives = cross_extension_shared_states::native_enum_type_map::get();
103+
auto found = natives.find(std::type_index(typeid(EnumType)));
104+
if (found != natives.end()) {
105+
if (!isinstance(src, found->second)) {
106+
return false;
107+
}
108+
type_caster<Underlying> underlying_caster;
109+
if (!underlying_caster.load(src.attr("value"), convert)) {
110+
pybind11_fail("native_enum internal consistency failure.");
111+
}
112+
value = static_cast<EnumType>(static_cast<Underlying>(underlying_caster));
113+
return true;
114+
}
115+
if (!pybind11_enum_) {
116+
pybind11_enum_.reset(new type_caster_for_class_<EnumType>());
117+
}
118+
return pybind11_enum_->load(src, convert);
119+
}
120+
121+
template <typename T>
122+
using cast_op_type = detail::cast_op_type<T>;
123+
124+
// NOLINTNEXTLINE(google-explicit-constructor)
125+
operator EnumType *() {
126+
if (!pybind11_enum_) {
127+
return &value;
128+
}
129+
return pybind11_enum_->operator EnumType *();
130+
}
131+
132+
// NOLINTNEXTLINE(google-explicit-constructor)
133+
operator EnumType &() {
134+
if (!pybind11_enum_) {
135+
return value;
136+
}
137+
return pybind11_enum_->operator EnumType &();
138+
}
139+
140+
private:
141+
std::unique_ptr<type_caster_for_class_<EnumType>> pybind11_enum_;
142+
EnumType value;
143+
};
144+
145+
template <typename EnumType, typename SFINAE = void>
146+
struct type_caster_enum_type_enabled : std::true_type {};
147+
148+
template <typename T>
149+
struct type_uses_type_caster_enum_type {
150+
static constexpr bool value
151+
= std::is_enum<T>::value && type_caster_enum_type_enabled<T>::value;
152+
};
153+
154+
template <typename EnumType>
155+
class type_caster<EnumType, detail::enable_if_t<type_uses_type_caster_enum_type<EnumType>::value>>
156+
: public type_caster_enum_type<EnumType> {};
157+
158+
template <typename T>
159+
struct type_uses_smart_holder_type_caster {
160+
static constexpr bool value
161+
= std::is_base_of<smart_holder_type_caster_base_tag, make_caster<T>>::value
162+
#ifdef PYBIND11_USE_SMART_HOLDER_AS_DEFAULT
163+
|| type_uses_type_caster_enum_type<T>::value
164+
#endif
165+
;
166+
};
167+
168+
template <typename T, typename SFINAE = void>
169+
struct type_caster_classh_enum_aware : type_caster<T> {};
170+
171+
#ifdef PYBIND11_USE_SMART_HOLDER_AS_DEFAULT
172+
template <typename EnumType>
173+
struct type_caster_classh_enum_aware<
174+
EnumType,
175+
detail::enable_if_t<type_uses_type_caster_enum_type<EnumType>::value>>
176+
: type_caster_for_class_<EnumType> {};
177+
#endif
178+
179+
template <typename T, detail::enable_if_t<std::is_enum<T>::value, int> = 0>
180+
bool isinstance_native_enum_impl(handle obj, const std::type_info &tp) {
181+
auto const &natives = cross_extension_shared_states::native_enum_type_map::get();
182+
auto found = natives.find(tp);
183+
if (found == natives.end()) {
184+
return false;
185+
}
186+
return isinstance(obj, found->second);
187+
}
188+
189+
template <typename T, detail::enable_if_t<!std::is_enum<T>::value, int> = 0>
190+
bool isinstance_native_enum_impl(handle, const std::type_info &) {
191+
return false;
192+
}
193+
194+
template <typename T>
195+
bool isinstance_native_enum(handle obj, const std::type_info &tp) {
196+
return isinstance_native_enum_impl<intrinsic_t<T>>(obj, tp);
197+
}
198+
84199
template <typename type>
85200
class type_caster<std::reference_wrapper<type>> {
86201
private:
@@ -1024,11 +1139,11 @@ using move_never = none_of<move_always<T>, move_if_unreferenced<T>>;
10241139
// non-reference/pointer `type`s and reference/pointers from a type_caster_generic are safe;
10251140
// everything else returns a reference/pointer to a local variable.
10261141
template <typename type>
1027-
using cast_is_temporary_value_reference
1028-
= bool_constant<(std::is_reference<type>::value || std::is_pointer<type>::value)
1029-
&& !std::is_base_of<type_caster_generic, make_caster<type>>::value
1030-
&& !type_uses_smart_holder_type_caster<intrinsic_t<type>>::value
1031-
&& !std::is_same<intrinsic_t<type>, void>::value>;
1142+
using cast_is_temporary_value_reference = bool_constant<
1143+
(std::is_reference<type>::value || std::is_pointer<type>::value)
1144+
&& !std::is_base_of<type_caster_generic, make_caster<type>>::value
1145+
&& !std::is_base_of<smart_holder_type_caster_base_tag, make_caster<type>>::value
1146+
&& !std::is_same<intrinsic_t<type>, void>::value>;
10321147

10331148
// When a value returned from a C++ function is being cast back to Python, we almost always want to
10341149
// force `policy = move`, regardless of the return value policy the function/method was declared
@@ -1086,8 +1201,18 @@ PYBIND11_NAMESPACE_END(detail)
10861201
template <typename T, detail::enable_if_t<!detail::is_pyobject<T>::value, int> = 0>
10871202
T cast(const handle &handle) {
10881203
using namespace detail;
1089-
static_assert(!cast_is_temporary_value_reference<T>::value,
1204+
constexpr bool is_enum_cast = type_uses_type_caster_enum_type<intrinsic_t<T>>::value;
1205+
static_assert(!cast_is_temporary_value_reference<T>::value || is_enum_cast,
10901206
"Unable to cast type to reference: value is local to type caster");
1207+
#ifndef NDEBUG
1208+
if (is_enum_cast && cast_is_temporary_value_reference<T>::value) {
1209+
if (cross_extension_shared_states::native_enum_type_map::get().count(
1210+
std::type_index(typeid(intrinsic_t<T>)))
1211+
!= 0) {
1212+
pybind11_fail("Unable to cast native enum type to reference");
1213+
}
1214+
}
1215+
#endif
10911216
return cast_op<T>(load_type<T>(handle));
10921217
}
10931218

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
// Copyright (c) 2022 The pybind Community.
2+
// All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
#pragma once
6+
7+
#include "../pytypes.h"
8+
#include "abi_platform_id.h"
9+
#include "common.h"
10+
#include "cross_extension_shared_state.h"
11+
#include "type_map.h"
12+
13+
#include <string>
14+
#include <typeindex>
15+
16+
PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE)
17+
PYBIND11_NAMESPACE_BEGIN(detail)
18+
19+
class native_enum_data {
20+
public:
21+
native_enum_data(const char *enum_name,
22+
const std::type_index &enum_type_index,
23+
bool use_int_enum)
24+
: enum_name_encoded{enum_name}, enum_type_index{enum_type_index},
25+
use_int_enum{use_int_enum}, enum_name{enum_name} {}
26+
27+
native_enum_data(const native_enum_data &) = delete;
28+
native_enum_data &operator=(const native_enum_data &) = delete;
29+
30+
void disarm_correct_use_check() const { correct_use_check = false; }
31+
void arm_correct_use_check() const { correct_use_check = true; }
32+
33+
// This is a separate public function only to enable easy unit testing.
34+
std::string was_not_added_error_message() const {
35+
return "`native_enum` was not added to any module."
36+
" Use e.g. `m += native_enum<...>(\""
37+
+ enum_name_encoded + "\")` to fix.";
38+
}
39+
40+
#if !defined(NDEBUG)
41+
// This dtor cannot easily be unit tested because it terminates the process.
42+
~native_enum_data() {
43+
if (correct_use_check) {
44+
pybind11_fail(was_not_added_error_message());
45+
}
46+
}
47+
#endif
48+
49+
private:
50+
mutable bool correct_use_check{false};
51+
52+
public:
53+
std::string enum_name_encoded;
54+
std::type_index enum_type_index;
55+
bool use_int_enum;
56+
bool export_values_flag{false};
57+
str enum_name;
58+
list members;
59+
list docs;
60+
};
61+
62+
PYBIND11_NAMESPACE_END(detail)
63+
64+
PYBIND11_NAMESPACE_BEGIN(cross_extension_shared_states)
65+
66+
struct native_enum_type_map_v1_adapter {
67+
static constexpr const char *abi_id() {
68+
return "__pybind11_native_enum_type_map_v1" PYBIND11_PLATFORM_ABI_ID_V4 "__";
69+
}
70+
71+
using payload_type = detail::type_map<PyObject *>;
72+
73+
static void payload_clear(payload_type &payload) {
74+
for (auto it : payload) {
75+
Py_DECREF(it.second);
76+
}
77+
payload.clear();
78+
}
79+
};
80+
81+
using native_enum_type_map_v1
82+
= detail::cross_extension_shared_state<native_enum_type_map_v1_adapter>;
83+
using native_enum_type_map = native_enum_type_map_v1;
84+
85+
PYBIND11_NAMESPACE_END(cross_extension_shared_states)
86+
87+
PYBIND11_NAMESPACE_BEGIN(detail)
88+
89+
inline void native_enum_add_to_parent(object parent, const detail::native_enum_data &data) {
90+
data.disarm_correct_use_check();
91+
if (hasattr(parent, data.enum_name)) {
92+
pybind11_fail("pybind11::native_enum<...>(\"" + data.enum_name_encoded
93+
+ "\"): an object with that name is already defined");
94+
}
95+
auto enum_module = reinterpret_steal<object>(PyImport_ImportModule("enum"));
96+
if (!enum_module) {
97+
raise_from(PyExc_SystemError,
98+
"`import enum` FAILED at " __FILE__ ":" PYBIND11_TOSTRING(__LINE__));
99+
}
100+
auto py_enum_type = enum_module.attr(data.use_int_enum ? "IntEnum" : "Enum");
101+
auto py_enum = py_enum_type(data.enum_name, data.members);
102+
if (hasattr(parent, "__module__")) {
103+
// Enum nested in class:
104+
py_enum.attr("__module__") = parent.attr("__module__");
105+
} else {
106+
py_enum.attr("__module__") = parent;
107+
}
108+
parent.attr(data.enum_name) = py_enum;
109+
if (data.export_values_flag) {
110+
for (auto member : data.members) {
111+
auto member_name = member[int_(0)];
112+
if (hasattr(parent, member_name)) {
113+
pybind11_fail("pybind11::native_enum<...>(\"" + data.enum_name_encoded
114+
+ "\").value(\"" + member_name.cast<std::string>()
115+
+ "\"): an object with that name is already defined");
116+
}
117+
parent.attr(member_name) = py_enum[member_name];
118+
}
119+
}
120+
for (auto doc : data.docs) {
121+
py_enum[doc[int_(0)]].attr("__doc__") = doc[int_(1)];
122+
}
123+
cross_extension_shared_states::native_enum_type_map::get()[data.enum_type_index]
124+
= py_enum.release().ptr();
125+
}
126+
127+
PYBIND11_NAMESPACE_END(detail)
128+
129+
PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE)

include/pybind11/detail/smart_holder_sfinae_hooks_only.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,13 @@
2121
PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE)
2222
PYBIND11_NAMESPACE_BEGIN(detail)
2323

24+
constexpr bool smart_holder_is_default_holder_type =
25+
#ifdef PYBIND11_USE_SMART_HOLDER_AS_DEFAULT
26+
true;
27+
#else
28+
false;
29+
#endif
30+
2431
template <typename T>
2532
struct is_smart_holder_type : std::false_type {};
2633

include/pybind11/embed.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,8 @@ inline void initialize_interpreter(bool init_signal_handlers = true,
243243
244244
\endrst */
245245
inline void finalize_interpreter() {
246+
cross_extension_shared_states::native_enum_type_map::scoped_clear native_enum_type_map_clear;
247+
246248
// Get the internals pointer (without creating it if it doesn't exist). It's possible for the
247249
// internals to be created during Py_Finalize() (e.g. if a py::capsule calls `get_internals()`
248250
// during destruction), so we get the pointer-pointer here and check it after Py_Finalize().

0 commit comments

Comments
 (0)