From 80d9936268927626b93f4eef813a6f4c374af022 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sat, 8 Mar 2025 13:32:44 -0800 Subject: [PATCH 01/29] Apply smart_holder-branch-based PR #5280 on top of master. --- CMakeLists.txt | 2 + docs/classes.rst | 5 + include/pybind11/cast.h | 110 ++++++++++- include/pybind11/detail/class.h | 10 +- include/pybind11/detail/internals.h | 8 +- include/pybind11/detail/native_enum_data.h | 123 ++++++++++++ include/pybind11/native_enum.h | 63 ++++++ include/pybind11/pybind11.h | 19 ++ include/pybind11/pytypes.h | 19 +- tests/CMakeLists.txt | 1 + tests/extra_python_package/test_files.py | 2 + tests/test_enum.cpp | 16 ++ tests/test_enum.py | 9 + tests/test_native_enum.cpp | 192 ++++++++++++++++++ tests/test_native_enum.py | 219 +++++++++++++++++++++ 15 files changed, 784 insertions(+), 14 deletions(-) create mode 100644 include/pybind11/detail/native_enum_data.h create mode 100644 include/pybind11/native_enum.h create mode 100644 tests/test_native_enum.cpp create mode 100644 tests/test_native_enum.py diff --git a/CMakeLists.txt b/CMakeLists.txt index 8783dba6e3..266e111723 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 @@ -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 diff --git a/docs/classes.rst b/docs/classes.rst index 5406668f0b..94e9e7b695 100644 --- a/docs/classes.rst +++ b/docs/classes.rst @@ -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 `_ for background). + +.. note:: + + ``py::native_enum`` was added as an alternative to ``py::enum_`` + with http://github.com/pybind/pybind11/pull/5555 diff --git a/include/pybind11/cast.h b/include/pybind11/cast.h index 47575084c8..caa1e9e851 100644 --- a/include/pybind11/cast.h +++ b/include/pybind11/cast.h @@ -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 &&caster) { return std::move(caster).operator result_t(); } +template +class type_caster_enum_type { +private: + using Underlying = typename std::underlying_type::type; + +public: + static constexpr auto name = const_name(); + + template + 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(src)).release(); + } + return type_caster_base::cast( + std::forward(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_caster; + if (!underlying_caster.load(src.attr("value"), convert)) { + pybind11_fail("native_enum internal consistency failure."); + } + value = static_cast(static_cast(underlying_caster)); + return true; + } + if (!pybind11_enum_) { + pybind11_enum_.reset(new type_caster_base()); + } + return pybind11_enum_->load(src, convert); + } + + template + using cast_op_type = detail::cast_op_type; + + // 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> pybind11_enum_; + EnumType value; +}; + +template +struct type_caster_enum_type_enabled : std::true_type {}; + +template +struct type_uses_type_caster_enum_type { + static constexpr bool value + = std::is_enum::value && type_caster_enum_type_enabled::value; +}; + +template +class type_caster::value>> + : public type_caster_enum_type {}; + +template ::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 ::value, int> = 0> +bool isinstance_native_enum_impl(handle, const std::type_info &) { + return false; +} + +template +bool isinstance_native_enum(handle obj, const std::type_info &tp) { + return isinstance_native_enum_impl>(obj, tp); +} + template class type_caster> { private: @@ -1468,8 +1567,17 @@ template T cast(const handle &handle) { using namespace detail; - static_assert(!cast_is_temporary_value_reference::value, + constexpr bool is_enum_cast = type_uses_type_caster_enum_type>::value; + static_assert(!cast_is_temporary_value_reference::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::value) { + if (detail::global_internals_native_enum_type_map_contains( + std::type_index(typeid(intrinsic_t)))) { + pybind11_fail("Unable to cast native enum type to reference"); + } + } +#endif return cast_op(load_type(handle)); } diff --git a/include/pybind11/detail/class.h b/include/pybind11/detail/class.h index 08e23afb59..98ff61f5d7 100644 --- a/include/pybind11/detail/class.h +++ b/include/pybind11/detail/class.h @@ -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() + "." + rec.name : diff --git a/include/pybind11/detail/internals.h b/include/pybind11/detail/internals.h index 841c8fe155..2fd1f6719c 100644 --- a/include/pybind11/detail/internals.h +++ b/include/pybind11/detail/internals.h @@ -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 native_enum_type_map; + internals() = default; internals(const internals &other) = delete; internals &operator=(const internals &other) = delete; diff --git a/include/pybind11/detail/native_enum_data.h b/include/pybind11/detail/native_enum_data.h new file mode 100644 index 0000000000..f5324a9803 --- /dev/null +++ b/include/pybind11/detail/native_enum_data.h @@ -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 +#include + +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(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() + + "\"): 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) diff --git a/include/pybind11/native_enum.h b/include/pybind11/native_enum.h new file mode 100644 index 0000000000..ac0c394164 --- /dev/null +++ b/include/pybind11/native_enum.h @@ -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 +#include +#include + +PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE) + +enum class native_enum_kind { Enum, IntEnum }; + +/// Conversions between Python's native (stdlib) enum types and C++ enums. +template +class native_enum : public detail::native_enum_data { +public: + using Underlying = typename std::underlying_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(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) diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index 4dee2c55fa..b201cd5ba6 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -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" @@ -1376,6 +1377,11 @@ class module_ : public object { // For Python 2, reinterpret_borrow was correct. return reinterpret_borrow(m); } + + module_ &operator+=(const detail::native_enum_data &data) { + detail::native_enum_add_to_parent(*this, data); + return *this; + } }; PYBIND11_NAMESPACE_BEGIN(detail) @@ -2186,6 +2192,11 @@ class class_ : public detail::generic_type { return *this; } + class_ &operator+=(const detail::native_enum_data &data) { + detail::native_enum_add_to_parent(*this, data); + return *this; + } + private: /// Initialize holder object, variant 1: object derives from enable_shared_from_this template @@ -2668,6 +2679,14 @@ class enum_ : public class_ { template enum_(const handle &scope, const char *name, const Extra &...extra) : class_(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...>::value; constexpr bool is_convertible = std::is_convertible::value; m_base.init(is_arithmetic, is_convertible); diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index 92e0a81f44..b38526e8f6 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -48,6 +48,9 @@ PYBIND11_NAMESPACE_BEGIN(detail) class args_proxy; bool isinstance_generic(handle obj, const std::type_info &tp); +template +bool isinstance_native_enum(handle obj, const std::type_info &tp); + // Accessor forward declarations template class accessor; @@ -859,7 +862,8 @@ bool isinstance(handle obj) { template ::value, int> = 0> bool isinstance(handle obj) { - return detail::isinstance_generic(obj, typeid(T)); + return detail::isinstance_native_enum(obj, typeid(T)) + || detail::isinstance_generic(obj, typeid(T)); } template <> @@ -2651,5 +2655,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) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index e2fab9c13d..1b65729dfd 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -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 diff --git a/tests/extra_python_package/test_files.py b/tests/extra_python_package/test_files.py index 5d7299c2ac..2fa47c614a 100644 --- a/tests/extra_python_package/test_files.py +++ b/tests/extra_python_package/test_files.py @@ -38,6 +38,7 @@ "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 @@ "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", diff --git a/tests/test_enum.cpp b/tests/test_enum.cpp index 2597b275ef..4ec0af7245 100644 --- a/tests/test_enum.cpp +++ b/tests/test_enum.cpp @@ -130,4 +130,20 @@ TEST_SUBMODULE(enums, m) { py::enum_(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(obj)) { + if (*obj.cast() == UnscopedEnum::ETwo) { + return 2; + } + return 1; + } + return 0; + }); +#endif } diff --git a/tests/test_enum.py b/tests/test_enum.py index 044ef1803e..697e28843f 100644 --- a/tests/test_enum.py +++ b/tests/test_enum.py @@ -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 diff --git a/tests/test_native_enum.cpp b/tests/test_native_enum.cpp new file mode 100644 index 0000000000..39ce62b1df --- /dev/null +++ b/tests/test_native_enum.cpp @@ -0,0 +1,192 @@ +#include + +#include "pybind11_tests.h" + +#include + +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 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 +struct is_proto_enum : std::false_type {}; + +enum some_proto_enum : int { Zero, One }; + +template <> +struct is_proto_enum : std::true_type {}; + +} // namespace test_native_enum + +PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE) +PYBIND11_NAMESPACE_BEGIN(detail) + +template +struct type_caster_enum_type_enabled< + ProtoEnumType, + detail::enable_if_t::value>> : std::false_type { +}; + +// https://github.com/pybind/pybind11_protobuf/blob/a50899c2eb604fc5f25deeb8901eff6231b8b3c0/pybind11_protobuf/enum_type_caster.h#L101-L105 +template +struct type_caster::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(0); + return true; + } + + PYBIND11_TYPE_CASTER(ProtoEnumType, const_name()); +}; + +PYBIND11_NAMESPACE_END(detail) +PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE) + +TEST_SUBMODULE(native_enum, m) { + using namespace test_native_enum; + + m += py::native_enum("smallenum", py::native_enum_kind::IntEnum) + .value("a", smallenum::a) + .value("b", smallenum::b) + .value("c", smallenum::c); + + m += py::native_enum("color", py::native_enum_kind::IntEnum) + .value("red", color::red) + .value("yellow", color::yellow) + .value("green", color::green) + .value("blue", color::blue); + + m += py::native_enum("altitude", py::native_enum_kind::Enum) + .value("high", altitude::high) + .value("low", altitude::low); + + m += py::native_enum("export_values", py::native_enum_kind::IntEnum) + .value("exv0", export_values::exv0) + .value("exv1", export_values::exv1) + .export_values(); + + m += py::native_enum("member_doc", py::native_enum_kind::IntEnum) + .value("mem0", member_doc::mem0, "docA") + .value("mem1", member_doc::mem1) + .value("mem2", member_doc::mem2, "docC"); + + py::class_ py_class_with_enum(m, "class_with_enum"); + py_class_with_enum + += py::native_enum("in_class", py::native_enum_kind::IntEnum) + .value("one", class_with_enum::in_class::one) + .value("two", class_with_enum::in_class::two); + + m.def("isinstance_color", [](const py::object &obj) { return py::isinstance(obj); }); + + m.def("pass_color", [](color e) { return static_cast(e); }); + m.def("return_color", [](int i) { return static_cast(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(); }); +#endif + + m.def("py_cast_color_handle", [](py::handle obj) { + // Exercises `if (is_enum_cast && cast_is_temporary_value_reference::value)` + // in `T cast(const handle &handle)` + auto e = py::cast(obj); + return static_cast(e); + }); + + m.def("native_enum_data_was_not_added_error_message", [](const char *enum_name) { + py::detail::native_enum_data data(enum_name, std::type_index(typeid(void)), false); + data.disarm_correct_use_check(); + return data.was_not_added_error_message(); + }); + + m.def("native_enum_ctor_malformed_utf8", [](const char *malformed_utf8) { + enum fake { x }; + py::native_enum{malformed_utf8, py::native_enum_kind::IntEnum}; + }); + + m.def("native_enum_value_malformed_utf8", [](const char *malformed_utf8) { + enum fake { x }; + py::native_enum("fake", py::native_enum_kind::IntEnum) + .value(malformed_utf8, fake::x); + }); + + m.def("double_registration_native_enum", [](py::module_ m) { + enum fake { x }; + m += py::native_enum("fake_double_registration_native_enum", + py::native_enum_kind::IntEnum) + .value("x", fake::x); + py::native_enum("fake_double_registration_native_enum", py::native_enum_kind::Enum); + }); + + m.def("native_enum_name_clash", [](py::module_ m) { + enum fake { x }; + m += py::native_enum("fake_native_enum_name_clash", py::native_enum_kind::IntEnum) + .value("x", fake::x); + }); + + m.def("native_enum_value_name_clash", [](py::module_ m) { + enum fake { x }; + m += py::native_enum("fake_native_enum_value_name_clash", + py::native_enum_kind::IntEnum) + .value("fake_native_enum_value_name_clash_x", fake::x) + .export_values(); + }); + + m.def("double_registration_enum_before_native_enum", [](const py::module_ &m) { + enum fake { x }; + py::enum_(m, "fake_enum_first").value("x", fake::x); + py::native_enum("fake_enum_first", py::native_enum_kind::IntEnum) + .value("x", fake::x); + }); + + m.def("double_registration_native_enum_before_enum", [](py::module_ m) { + enum fake { x }; + m += py::native_enum("fake_native_enum_first", py::native_enum_kind::IntEnum) + .value("x", fake::x); + py::enum_(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_correct_use_failure", []() { + enum fake { x }; + py::native_enum("fake_native_enum_correct_use_failure", + py::native_enum_kind::IntEnum) + .value("x", fake::x); + }); +#else + m.attr("native_enum_correct_use_failure") = "For local testing only: terminates process"; +#endif +} diff --git a/tests/test_native_enum.py b/tests/test_native_enum.py new file mode 100644 index 0000000000..3d7025309e --- /dev/null +++ b/tests/test_native_enum.py @@ -0,0 +1,219 @@ +from __future__ import annotations + +import enum +import pickle + +import pytest + +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"), +) + +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.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 + + +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_native_enum_data_was_not_added_error_message(): + msg = m.native_enum_data_was_not_added_error_message("Fake") + assert msg == ( + "`native_enum` was not added to any module." + ' Use e.g. `m += native_enum<...>("Fake", ...)` to fix.' + ) + + +@pytest.mark.parametrize( + "func", [m.native_enum_ctor_malformed_utf8, m.native_enum_value_malformed_utf8] +) +def test_native_enum_malformed_utf8(func): + malformed_utf8 = b"\x80" + with pytest.raises(UnicodeDecodeError): + func(malformed_utf8) + + +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_correct_use_failure(): + if not isinstance(m.native_enum_correct_use_failure, str): + m.native_enum_correct_use_failure() + pytest.fail("Process termination expected.") From 06eddc7179653200a0b1de9b1ccab44d9e7cdb8a Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sat, 8 Mar 2025 14:39:11 -0800 Subject: [PATCH 02/29] Add pytest.skip("GraalPy does not raise UnicodeDecodeError") --- tests/test_native_enum.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_native_enum.py b/tests/test_native_enum.py index 3d7025309e..15dd8a7d6d 100644 --- a/tests/test_native_enum.py +++ b/tests/test_native_enum.py @@ -5,6 +5,7 @@ import pytest +import env from pybind11_tests import native_enum as m SMALLENUM_MEMBERS = ( @@ -156,6 +157,8 @@ def test_native_enum_data_was_not_added_error_message(): "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) From ce5859e20869471f18569cc8d71f9a39252feb39 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sun, 9 Mar 2025 12:49:49 -0700 Subject: [PATCH 03/29] Add `parent_scope` as first argument to `py::native_enum` ctor. --- include/pybind11/detail/native_enum_data.h | 8 +++-- include/pybind11/native_enum.h | 8 +++-- tests/test_native_enum.cpp | 41 ++++++++++++---------- 3 files changed, 32 insertions(+), 25 deletions(-) diff --git a/include/pybind11/detail/native_enum_data.h b/include/pybind11/detail/native_enum_data.h index f5324a9803..f9ef5761ed 100644 --- a/include/pybind11/detail/native_enum_data.h +++ b/include/pybind11/detail/native_enum_data.h @@ -18,11 +18,12 @@ PYBIND11_NAMESPACE_BEGIN(detail) class native_enum_data { public: - native_enum_data(const char *enum_name, + native_enum_data(object parent_scope, + 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} {} + : parent_scope(parent_scope), 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; @@ -50,6 +51,7 @@ class native_enum_data { mutable bool correct_use_check{false}; public: + object parent_scope; std::string enum_name_encoded; std::type_index enum_type_index; bool use_int_enum; diff --git a/include/pybind11/native_enum.h b/include/pybind11/native_enum.h index ac0c394164..30b87971e0 100644 --- a/include/pybind11/native_enum.h +++ b/include/pybind11/native_enum.h @@ -23,9 +23,11 @@ class native_enum : public detail::native_enum_data { public: using Underlying = typename std::underlying_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) { + explicit native_enum(object parent_scope, const char *name, native_enum_kind kind) + : detail::native_enum_data(parent_scope, + 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( diff --git a/tests/test_native_enum.cpp b/tests/test_native_enum.cpp index 39ce62b1df..636ef08314 100644 --- a/tests/test_native_enum.cpp +++ b/tests/test_native_enum.cpp @@ -71,34 +71,34 @@ PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE) TEST_SUBMODULE(native_enum, m) { using namespace test_native_enum; - m += py::native_enum("smallenum", py::native_enum_kind::IntEnum) + m += py::native_enum(m, "smallenum", py::native_enum_kind::IntEnum) .value("a", smallenum::a) .value("b", smallenum::b) .value("c", smallenum::c); - m += py::native_enum("color", py::native_enum_kind::IntEnum) + m += py::native_enum(m, "color", py::native_enum_kind::IntEnum) .value("red", color::red) .value("yellow", color::yellow) .value("green", color::green) .value("blue", color::blue); - m += py::native_enum("altitude", py::native_enum_kind::Enum) + m += py::native_enum(m, "altitude", py::native_enum_kind::Enum) .value("high", altitude::high) .value("low", altitude::low); - m += py::native_enum("export_values", py::native_enum_kind::IntEnum) + m += py::native_enum(m, "export_values", py::native_enum_kind::IntEnum) .value("exv0", export_values::exv0) .value("exv1", export_values::exv1) .export_values(); - m += py::native_enum("member_doc", py::native_enum_kind::IntEnum) + m += py::native_enum(m, "member_doc", py::native_enum_kind::IntEnum) .value("mem0", member_doc::mem0, "docA") .value("mem1", member_doc::mem1) .value("mem2", member_doc::mem2, "docC"); py::class_ py_class_with_enum(m, "class_with_enum"); py_class_with_enum - += py::native_enum("in_class", py::native_enum_kind::IntEnum) + += py::native_enum(m, "in_class", py::native_enum_kind::IntEnum) .value("one", class_with_enum::in_class::one) .value("two", class_with_enum::in_class::two); @@ -127,40 +127,43 @@ TEST_SUBMODULE(native_enum, m) { }); m.def("native_enum_data_was_not_added_error_message", [](const char *enum_name) { - py::detail::native_enum_data data(enum_name, std::type_index(typeid(void)), false); + py::detail::native_enum_data data( + py::none(), enum_name, std::type_index(typeid(void)), false); data.disarm_correct_use_check(); return data.was_not_added_error_message(); }); m.def("native_enum_ctor_malformed_utf8", [](const char *malformed_utf8) { enum fake { x }; - py::native_enum{malformed_utf8, py::native_enum_kind::IntEnum}; + py::native_enum{py::none(), malformed_utf8, py::native_enum_kind::IntEnum}; }); m.def("native_enum_value_malformed_utf8", [](const char *malformed_utf8) { enum fake { x }; - py::native_enum("fake", py::native_enum_kind::IntEnum) + py::native_enum(py::none(), "fake", py::native_enum_kind::IntEnum) .value(malformed_utf8, fake::x); }); m.def("double_registration_native_enum", [](py::module_ m) { enum fake { x }; - m += py::native_enum("fake_double_registration_native_enum", - py::native_enum_kind::IntEnum) + m += py::native_enum( + py::none(), "fake_double_registration_native_enum", py::native_enum_kind::IntEnum) .value("x", fake::x); - py::native_enum("fake_double_registration_native_enum", py::native_enum_kind::Enum); + py::native_enum( + py::none(), "fake_double_registration_native_enum", py::native_enum_kind::Enum); }); m.def("native_enum_name_clash", [](py::module_ m) { enum fake { x }; - m += py::native_enum("fake_native_enum_name_clash", py::native_enum_kind::IntEnum) + m += py::native_enum( + py::none(), "fake_native_enum_name_clash", py::native_enum_kind::IntEnum) .value("x", fake::x); }); m.def("native_enum_value_name_clash", [](py::module_ m) { enum fake { x }; - m += py::native_enum("fake_native_enum_value_name_clash", - py::native_enum_kind::IntEnum) + m += py::native_enum( + py::none(), "fake_native_enum_value_name_clash", py::native_enum_kind::IntEnum) .value("fake_native_enum_value_name_clash_x", fake::x) .export_values(); }); @@ -168,13 +171,13 @@ TEST_SUBMODULE(native_enum, m) { m.def("double_registration_enum_before_native_enum", [](const py::module_ &m) { enum fake { x }; py::enum_(m, "fake_enum_first").value("x", fake::x); - py::native_enum("fake_enum_first", py::native_enum_kind::IntEnum) + py::native_enum(py::none(), "fake_enum_first", py::native_enum_kind::IntEnum) .value("x", fake::x); }); m.def("double_registration_native_enum_before_enum", [](py::module_ m) { enum fake { x }; - m += py::native_enum("fake_native_enum_first", py::native_enum_kind::IntEnum) + m += py::native_enum(m, "fake_native_enum_first", py::native_enum_kind::IntEnum) .value("x", fake::x); py::enum_(m, "name_must_be_different_to_reach_desired_code_path"); }); @@ -182,8 +185,8 @@ TEST_SUBMODULE(native_enum, m) { #if defined(PYBIND11_NEGATE_THIS_CONDITION_FOR_LOCAL_TESTING) && !defined(NDEBUG) m.def("native_enum_correct_use_failure", []() { enum fake { x }; - py::native_enum("fake_native_enum_correct_use_failure", - py::native_enum_kind::IntEnum) + py::native_enum( + py::none(), "fake_native_enum_correct_use_failure", py::native_enum_kind::IntEnum) .value("x", fake::x); }); #else From 943d0667ad3d89f9ffbef04bdb4c398a5bce2c57 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sun, 9 Mar 2025 13:25:18 -0700 Subject: [PATCH 04/29] Replace `operator+=` API with `.finalize()` API. The error messages still need cleanup. --- include/pybind11/detail/native_enum_data.h | 4 + include/pybind11/pybind11.h | 10 --- tests/test_native_enum.cpp | 99 ++++++++++++---------- 3 files changed, 59 insertions(+), 54 deletions(-) diff --git a/include/pybind11/detail/native_enum_data.h b/include/pybind11/detail/native_enum_data.h index f9ef5761ed..118aaa4725 100644 --- a/include/pybind11/detail/native_enum_data.h +++ b/include/pybind11/detail/native_enum_data.h @@ -25,6 +25,8 @@ class native_enum_data { : parent_scope(parent_scope), enum_name_encoded{enum_name}, enum_type_index{enum_type_index}, use_int_enum{use_int_enum}, enum_name{enum_name} {} + void finalize(); + native_enum_data(const native_enum_data &) = delete; native_enum_data &operator=(const native_enum_data &) = delete; @@ -121,5 +123,7 @@ inline void native_enum_add_to_parent(const object &parent, const detail::native global_internals_native_enum_type_map_set_item(data.enum_type_index, py_enum.release().ptr()); } +inline void native_enum_data::finalize() { native_enum_add_to_parent(parent_scope, *this); } + PYBIND11_NAMESPACE_END(detail) PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE) diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index b201cd5ba6..19096e84b2 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -1377,11 +1377,6 @@ class module_ : public object { // For Python 2, reinterpret_borrow was correct. return reinterpret_borrow(m); } - - module_ &operator+=(const detail::native_enum_data &data) { - detail::native_enum_add_to_parent(*this, data); - return *this; - } }; PYBIND11_NAMESPACE_BEGIN(detail) @@ -2192,11 +2187,6 @@ class class_ : public detail::generic_type { return *this; } - class_ &operator+=(const detail::native_enum_data &data) { - detail::native_enum_add_to_parent(*this, data); - return *this; - } - private: /// Initialize holder object, variant 1: object derives from enable_shared_from_this template diff --git a/tests/test_native_enum.cpp b/tests/test_native_enum.cpp index 636ef08314..3275984682 100644 --- a/tests/test_native_enum.cpp +++ b/tests/test_native_enum.cpp @@ -71,36 +71,42 @@ PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE) TEST_SUBMODULE(native_enum, m) { using namespace test_native_enum; - m += py::native_enum(m, "smallenum", py::native_enum_kind::IntEnum) - .value("a", smallenum::a) - .value("b", smallenum::b) - .value("c", smallenum::c); - - m += py::native_enum(m, "color", py::native_enum_kind::IntEnum) - .value("red", color::red) - .value("yellow", color::yellow) - .value("green", color::green) - .value("blue", color::blue); - - m += py::native_enum(m, "altitude", py::native_enum_kind::Enum) - .value("high", altitude::high) - .value("low", altitude::low); - - m += py::native_enum(m, "export_values", py::native_enum_kind::IntEnum) - .value("exv0", export_values::exv0) - .value("exv1", export_values::exv1) - .export_values(); - - m += py::native_enum(m, "member_doc", py::native_enum_kind::IntEnum) - .value("mem0", member_doc::mem0, "docA") - .value("mem1", member_doc::mem1) - .value("mem2", member_doc::mem2, "docC"); + py::native_enum(m, "smallenum", py::native_enum_kind::IntEnum) + .value("a", smallenum::a) + .value("b", smallenum::b) + .value("c", smallenum::c) + .finalize(); + + py::native_enum(m, "color", py::native_enum_kind::IntEnum) + .value("red", color::red) + .value("yellow", color::yellow) + .value("green", color::green) + .value("blue", color::blue) + .finalize(); + + py::native_enum(m, "altitude", py::native_enum_kind::Enum) + .value("high", altitude::high) + .value("low", altitude::low) + .finalize(); + + py::native_enum(m, "export_values", py::native_enum_kind::IntEnum) + .value("exv0", export_values::exv0) + .value("exv1", export_values::exv1) + .export_values() + .finalize(); + + py::native_enum(m, "member_doc", py::native_enum_kind::IntEnum) + .value("mem0", member_doc::mem0, "docA") + .value("mem1", member_doc::mem1) + .value("mem2", member_doc::mem2, "docC") + .finalize(); py::class_ py_class_with_enum(m, "class_with_enum"); - py_class_with_enum - += py::native_enum(m, "in_class", py::native_enum_kind::IntEnum) - .value("one", class_with_enum::in_class::one) - .value("two", class_with_enum::in_class::two); + py::native_enum( + py_class_with_enum, "in_class", py::native_enum_kind::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(obj); }); @@ -146,39 +152,43 @@ TEST_SUBMODULE(native_enum, m) { m.def("double_registration_native_enum", [](py::module_ m) { enum fake { x }; - m += py::native_enum( - py::none(), "fake_double_registration_native_enum", py::native_enum_kind::IntEnum) - .value("x", fake::x); py::native_enum( - py::none(), "fake_double_registration_native_enum", py::native_enum_kind::Enum); + m, "fake_double_registration_native_enum", py::native_enum_kind::IntEnum) + .value("x", fake::x) + .finalize(); + py::native_enum( + m, "fake_double_registration_native_enum", py::native_enum_kind::Enum); }); m.def("native_enum_name_clash", [](py::module_ m) { enum fake { x }; - m += py::native_enum( - py::none(), "fake_native_enum_name_clash", py::native_enum_kind::IntEnum) - .value("x", fake::x); + py::native_enum(m, "fake_native_enum_name_clash", py::native_enum_kind::IntEnum) + .value("x", fake::x) + .finalize(); }); m.def("native_enum_value_name_clash", [](py::module_ m) { enum fake { x }; - m += py::native_enum( - py::none(), "fake_native_enum_value_name_clash", py::native_enum_kind::IntEnum) - .value("fake_native_enum_value_name_clash_x", fake::x) - .export_values(); + py::native_enum( + m, "fake_native_enum_value_name_clash", py::native_enum_kind::IntEnum) + .value("fake_native_enum_value_name_clash_x", fake::x) + .export_values() + .finalize(); }); m.def("double_registration_enum_before_native_enum", [](const py::module_ &m) { enum fake { x }; py::enum_(m, "fake_enum_first").value("x", fake::x); - py::native_enum(py::none(), "fake_enum_first", py::native_enum_kind::IntEnum) - .value("x", fake::x); + py::native_enum(m, "fake_enum_first", py::native_enum_kind::IntEnum) + .value("x", fake::x) + .finalize(); }); m.def("double_registration_native_enum_before_enum", [](py::module_ m) { enum fake { x }; - m += py::native_enum(m, "fake_native_enum_first", py::native_enum_kind::IntEnum) - .value("x", fake::x); + py::native_enum(m, "fake_native_enum_first", py::native_enum_kind::IntEnum) + .value("x", fake::x) + .finalize(); py::enum_(m, "name_must_be_different_to_reach_desired_code_path"); }); @@ -187,7 +197,8 @@ TEST_SUBMODULE(native_enum, m) { enum fake { x }; py::native_enum( py::none(), "fake_native_enum_correct_use_failure", py::native_enum_kind::IntEnum) - .value("x", fake::x); + .value("x", fake::x) + .finalize(); }); #else m.attr("native_enum_correct_use_failure") = "For local testing only: terminates process"; From 7689e5bb4bd7a25122b7885e6da145c72d560f50 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sun, 9 Mar 2025 14:21:16 -0700 Subject: [PATCH 05/29] Resolve clang-tidy performance-unnecessary-value-param errors --- include/pybind11/detail/native_enum_data.h | 2 +- include/pybind11/native_enum.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/include/pybind11/detail/native_enum_data.h b/include/pybind11/detail/native_enum_data.h index 118aaa4725..79085c5580 100644 --- a/include/pybind11/detail/native_enum_data.h +++ b/include/pybind11/detail/native_enum_data.h @@ -18,7 +18,7 @@ PYBIND11_NAMESPACE_BEGIN(detail) class native_enum_data { public: - native_enum_data(object parent_scope, + native_enum_data(const object &parent_scope, const char *enum_name, const std::type_index &enum_type_index, bool use_int_enum) diff --git a/include/pybind11/native_enum.h b/include/pybind11/native_enum.h index 30b87971e0..8f44a66c5c 100644 --- a/include/pybind11/native_enum.h +++ b/include/pybind11/native_enum.h @@ -23,7 +23,7 @@ class native_enum : public detail::native_enum_data { public: using Underlying = typename std::underlying_type::type; - explicit native_enum(object parent_scope, const char *name, native_enum_kind kind) + explicit native_enum(const object &parent_scope, const char *name, native_enum_kind kind) : detail::native_enum_data(parent_scope, name, std::type_index(typeid(Type)), From 1a422577bddaabe1324d58d2d47893a138cdea69 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sun, 9 Mar 2025 14:28:47 -0700 Subject: [PATCH 06/29] Rename (effectively) native_enum_add_to_parent() -> finalize() --- include/pybind11/detail/native_enum_data.h | 34 ++++++++++------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/include/pybind11/detail/native_enum_data.h b/include/pybind11/detail/native_enum_data.h index 79085c5580..958170de62 100644 --- a/include/pybind11/detail/native_enum_data.h +++ b/include/pybind11/detail/native_enum_data.h @@ -87,10 +87,10 @@ global_internals_native_enum_type_map_contains(const std::type_index &enum_type_ }); } -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 +inline void native_enum_data::finalize() { + disarm_correct_use_check(); + if (hasattr(parent_scope, enum_name)) { + pybind11_fail("pybind11::native_enum<...>(\"" + enum_name_encoded + "\"): an object with that name is already defined"); } auto enum_module = reinterpret_steal(PyImport_ImportModule("enum")); @@ -99,31 +99,29 @@ inline void native_enum_add_to_parent(const object &parent, const detail::native "`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); + auto py_enum_type = enum_module.attr(use_int_enum ? "IntEnum" : "Enum"); + 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.attr(data.enum_name) = py_enum; - if (data.export_values_flag) { - for (auto member : data.members) { + parent_scope.attr(enum_name) = py_enum; + if (export_values_flag) { + for (auto member : 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() + if (hasattr(parent_scope, member_name)) { + pybind11_fail("pybind11::native_enum<...>(\"" + enum_name_encoded + "\").value(\"" + + member_name.cast() + "\"): an object with that name is already defined"); } - parent.attr(member_name) = py_enum[member_name]; + parent_scope.attr(member_name) = py_enum[member_name]; } } - for (auto doc : data.docs) { + for (auto doc : 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()); + global_internals_native_enum_type_map_set_item(enum_type_index, py_enum.release().ptr()); } -inline void native_enum_data::finalize() { native_enum_add_to_parent(parent_scope, *this); } - PYBIND11_NAMESPACE_END(detail) PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE) From bab33489cdda1cdd55d5b23989533bfb83e86919 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sun, 9 Mar 2025 14:51:54 -0700 Subject: [PATCH 07/29] Update error message: pybind11::native_enum<...>("Fake", ...): MISSING .finalize() --- include/pybind11/detail/native_enum_data.h | 9 ++++----- tests/test_native_enum.cpp | 4 ++-- tests/test_native_enum.py | 9 +++------ 3 files changed, 9 insertions(+), 13 deletions(-) diff --git a/include/pybind11/detail/native_enum_data.h b/include/pybind11/detail/native_enum_data.h index 958170de62..d65a80fcf7 100644 --- a/include/pybind11/detail/native_enum_data.h +++ b/include/pybind11/detail/native_enum_data.h @@ -34,17 +34,16 @@ class native_enum_data { 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."; + std::string missing_finalize_error_message() const { + return "pybind11::native_enum<...>(\"" + enum_name_encoded + + "\", ...): MISSING .finalize()"; } #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()); + pybind11_fail(missing_finalize_error_message()); } } #endif diff --git a/tests/test_native_enum.cpp b/tests/test_native_enum.cpp index 3275984682..8d8ef2b81b 100644 --- a/tests/test_native_enum.cpp +++ b/tests/test_native_enum.cpp @@ -132,11 +132,11 @@ TEST_SUBMODULE(native_enum, m) { return static_cast(e); }); - m.def("native_enum_data_was_not_added_error_message", [](const char *enum_name) { + m.def("native_enum_data_missing_finalize_error_message", [](const char *enum_name) { py::detail::native_enum_data data( py::none(), enum_name, std::type_index(typeid(void)), false); data.disarm_correct_use_check(); - return data.was_not_added_error_message(); + return data.missing_finalize_error_message(); }); m.def("native_enum_ctor_malformed_utf8", [](const char *malformed_utf8) { diff --git a/tests/test_native_enum.py b/tests/test_native_enum.py index 15dd8a7d6d..f4c39a8f0a 100644 --- a/tests/test_native_enum.py +++ b/tests/test_native_enum.py @@ -145,12 +145,9 @@ def test_py_cast_color_handle(): assert m.py_cast_color_handle(m.color[name]) == value -def test_native_enum_data_was_not_added_error_message(): - msg = m.native_enum_data_was_not_added_error_message("Fake") - assert msg == ( - "`native_enum` was not added to any module." - ' Use e.g. `m += native_enum<...>("Fake", ...)` to fix.' - ) +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( From c22104f289242b9bbdf579b6cf85089abb647f75 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sun, 9 Mar 2025 15:03:47 -0700 Subject: [PATCH 08/29] Pass py::module_ by reference to resolve clang-tidy errors (this is entirely inconsequential otherwise for all practical purposes). --- tests/test_native_enum.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_native_enum.cpp b/tests/test_native_enum.cpp index 8d8ef2b81b..c3e099e3d3 100644 --- a/tests/test_native_enum.cpp +++ b/tests/test_native_enum.cpp @@ -150,7 +150,7 @@ TEST_SUBMODULE(native_enum, m) { .value(malformed_utf8, fake::x); }); - m.def("double_registration_native_enum", [](py::module_ m) { + m.def("double_registration_native_enum", [](py::module_ &m) { enum fake { x }; py::native_enum( m, "fake_double_registration_native_enum", py::native_enum_kind::IntEnum) @@ -160,14 +160,14 @@ TEST_SUBMODULE(native_enum, m) { m, "fake_double_registration_native_enum", py::native_enum_kind::Enum); }); - m.def("native_enum_name_clash", [](py::module_ m) { + m.def("native_enum_name_clash", [](py::module_ &m) { enum fake { x }; py::native_enum(m, "fake_native_enum_name_clash", py::native_enum_kind::IntEnum) .value("x", fake::x) .finalize(); }); - m.def("native_enum_value_name_clash", [](py::module_ m) { + m.def("native_enum_value_name_clash", [](py::module_ &m) { enum fake { x }; py::native_enum( m, "fake_native_enum_value_name_clash", py::native_enum_kind::IntEnum) @@ -176,7 +176,7 @@ TEST_SUBMODULE(native_enum, m) { .finalize(); }); - m.def("double_registration_enum_before_native_enum", [](const py::module_ &m) { + m.def("double_registration_enum_before_native_enum", [](py::module_ &m) { enum fake { x }; py::enum_(m, "fake_enum_first").value("x", fake::x); py::native_enum(m, "fake_enum_first", py::native_enum_kind::IntEnum) @@ -184,7 +184,7 @@ TEST_SUBMODULE(native_enum, m) { .finalize(); }); - m.def("double_registration_native_enum_before_enum", [](py::module_ m) { + m.def("double_registration_native_enum_before_enum", [](py::module_ &m) { enum fake { x }; py::native_enum(m, "fake_native_enum_first", py::native_enum_kind::IntEnum) .value("x", fake::x) From 0da489d112ad11c8f2dfa538cdf940d1af72be44 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sun, 9 Mar 2025 15:26:52 -0700 Subject: [PATCH 09/29] test_native_enum_correct_use_failure -> test_native_enum_missing_finalize_failure --- tests/test_native_enum.cpp | 9 +++++---- tests/test_native_enum.py | 6 +++--- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/tests/test_native_enum.cpp b/tests/test_native_enum.cpp index c3e099e3d3..96030454db 100644 --- a/tests/test_native_enum.cpp +++ b/tests/test_native_enum.cpp @@ -193,14 +193,15 @@ TEST_SUBMODULE(native_enum, m) { }); #if defined(PYBIND11_NEGATE_THIS_CONDITION_FOR_LOCAL_TESTING) && !defined(NDEBUG) - m.def("native_enum_correct_use_failure", []() { + m.def("native_enum_missing_finalize_failure", []() { enum fake { x }; py::native_enum( - py::none(), "fake_native_enum_correct_use_failure", py::native_enum_kind::IntEnum) + py::none(), "fake_native_enum_missing_finalize_failure", py::native_enum_kind::IntEnum) .value("x", fake::x) - .finalize(); + // .finalize() missing + ; }); #else - m.attr("native_enum_correct_use_failure") = "For local testing only: terminates process"; + m.attr("native_enum_missing_finalize_failure") = "For local testing only: terminates process"; #endif } diff --git a/tests/test_native_enum.py b/tests/test_native_enum.py index f4c39a8f0a..f4d92c0f68 100644 --- a/tests/test_native_enum.py +++ b/tests/test_native_enum.py @@ -213,7 +213,7 @@ def test_double_registration_native_enum_before_enum(): ) -def test_native_enum_correct_use_failure(): - if not isinstance(m.native_enum_correct_use_failure, str): - m.native_enum_correct_use_failure() +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.") From fb10d3b4dc01d3fc7d6b9f13b4e6f676a8772974 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sun, 9 Mar 2025 20:21:53 -0700 Subject: [PATCH 10/29] Add test_native_enum_double_finalize(), test_native_enum_value_after_finalize() --- include/pybind11/detail/native_enum_data.h | 21 ++++++++++++++++----- include/pybind11/native_enum.h | 7 ++++--- tests/test_native_enum.cpp | 17 ++++++++++++++++- tests/test_native_enum.py | 18 ++++++++++++++++++ 4 files changed, 54 insertions(+), 9 deletions(-) diff --git a/include/pybind11/detail/native_enum_data.h b/include/pybind11/detail/native_enum_data.h index d65a80fcf7..c3eb4f4a00 100644 --- a/include/pybind11/detail/native_enum_data.h +++ b/include/pybind11/detail/native_enum_data.h @@ -10,6 +10,7 @@ #include "common.h" #include "internals.h" +#include #include #include @@ -30,8 +31,18 @@ class native_enum_data { 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; } + void disarm_finalize_check(const char *error_context) const { + if (!finalize_needed) { + pybind11_fail("pybind11::native_enum<...>(\"" + enum_name_encoded + + "\"): " + error_context); + } + finalize_needed = false; + } + + void arm_finalize_check() const { + assert(!finalize_needed); + finalize_needed = true; + } // This is a separate public function only to enable easy unit testing. std::string missing_finalize_error_message() const { @@ -42,14 +53,14 @@ class native_enum_data { #if !defined(NDEBUG) // This dtor cannot easily be unit tested because it terminates the process. ~native_enum_data() { - if (correct_use_check) { + if (finalize_needed) { pybind11_fail(missing_finalize_error_message()); } } #endif private: - mutable bool correct_use_check{false}; + mutable bool finalize_needed{false}; public: object parent_scope; @@ -87,7 +98,7 @@ global_internals_native_enum_type_map_contains(const std::type_index &enum_type_ } inline void native_enum_data::finalize() { - disarm_correct_use_check(); + 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"); diff --git a/include/pybind11/native_enum.h b/include/pybind11/native_enum.h index 8f44a66c5c..6ea4aa51f1 100644 --- a/include/pybind11/native_enum.h +++ b/include/pybind11/native_enum.h @@ -38,7 +38,7 @@ class native_enum : public detail::native_enum_data { pybind11_fail("pybind11::native_enum<...>(\"" + enum_name_encoded + "\") is already registered!"); } - arm_correct_use_check(); + arm_finalize_check(); } /// Export enumeration entries into the parent scope @@ -49,12 +49,13 @@ class native_enum : public detail::native_enum_data { /// Add an enumeration entry native_enum &value(char const *name, Type value, const char *doc = nullptr) { - disarm_correct_use_check(); + // 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(value))); if (doc) { docs.append(make_tuple(name, doc)); } - arm_correct_use_check(); + arm_finalize_check(); // There was no exception. return *this; } diff --git a/tests/test_native_enum.cpp b/tests/test_native_enum.cpp index 96030454db..403598fa8a 100644 --- a/tests/test_native_enum.cpp +++ b/tests/test_native_enum.cpp @@ -135,7 +135,6 @@ TEST_SUBMODULE(native_enum, m) { m.def("native_enum_data_missing_finalize_error_message", [](const char *enum_name) { py::detail::native_enum_data data( py::none(), enum_name, std::type_index(typeid(void)), false); - data.disarm_correct_use_check(); return data.missing_finalize_error_message(); }); @@ -144,6 +143,22 @@ TEST_SUBMODULE(native_enum, m) { py::native_enum{py::none(), malformed_utf8, py::native_enum_kind::IntEnum}; }); + m.def("native_enum_double_finalize", [](py::module_ &m) { + enum fake { x }; + py::native_enum ne( + m, "fake_native_enum_double_finalize", py::native_enum_kind::IntEnum); + ne.finalize(); + ne.finalize(); + }); + + m.def("native_enum_value_after_finalize", [](py::module_ &m) { + enum fake { x }; + py::native_enum ne( + m, "fake_native_enum_value_after_finalize", py::native_enum_kind::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(py::none(), "fake", py::native_enum_kind::IntEnum) diff --git a/tests/test_native_enum.py b/tests/test_native_enum.py index f4d92c0f68..1ad51a6fcb 100644 --- a/tests/test_native_enum.py +++ b/tests/test_native_enum.py @@ -161,6 +161,24 @@ def test_native_enum_malformed_utf8(func): 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) From 7acb18a77123ad04670e373570b534ce32be1a74 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sun, 9 Mar 2025 21:01:16 -0700 Subject: [PATCH 11/29] Clean up public/protected API. --- include/pybind11/detail/native_enum_data.h | 57 ++++++++++++---------- tests/test_native_enum.cpp | 9 ++-- 2 files changed, 35 insertions(+), 31 deletions(-) diff --git a/include/pybind11/detail/native_enum_data.h b/include/pybind11/detail/native_enum_data.h index c3eb4f4a00..c04624757b 100644 --- a/include/pybind11/detail/native_enum_data.h +++ b/include/pybind11/detail/native_enum_data.h @@ -17,21 +17,38 @@ 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 std::type_index &enum_type_index, bool use_int_enum) - : parent_scope(parent_scope), enum_name_encoded{enum_name}, - enum_type_index{enum_type_index}, use_int_enum{use_int_enum}, enum_name{enum_name} {} + : enum_name_encoded{enum_name}, enum_type_index{enum_type_index}, enum_name{enum_name}, + parent_scope(parent_scope), export_values_flag{false}, use_int_enum{use_int_enum}, + finalize_needed{false} {} void finalize(); native_enum_data(const native_enum_data &) = delete; native_enum_data &operator=(const native_enum_data &) = delete; - void disarm_finalize_check(const char *error_context) const { +#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); @@ -39,38 +56,26 @@ class native_enum_data { finalize_needed = false; } - void arm_finalize_check() const { + void arm_finalize_check() { assert(!finalize_needed); finalize_needed = true; } - // This is a separate public function only to enable easy unit testing. - std::string missing_finalize_error_message() const { - return "pybind11::native_enum<...>(\"" + enum_name_encoded - + "\", ...): MISSING .finalize()"; - } - -#if !defined(NDEBUG) - // This dtor cannot easily be unit tested because it terminates the process. - ~native_enum_data() { - if (finalize_needed) { - pybind11_fail(missing_finalize_error_message()); - } - } -#endif - -private: - mutable bool finalize_needed{false}; - -public: - object parent_scope; std::string enum_name_encoded; std::type_index enum_type_index; - bool use_int_enum; - bool export_values_flag{false}; + +private: str enum_name; + object parent_scope; + +protected: list members; list docs; + bool export_values_flag : 1; + +private: + bool use_int_enum : 1; + bool finalize_needed : 1; }; inline void global_internals_native_enum_type_map_set_item(const std::type_index &enum_type_index, diff --git a/tests/test_native_enum.cpp b/tests/test_native_enum.cpp index 403598fa8a..801d874496 100644 --- a/tests/test_native_enum.cpp +++ b/tests/test_native_enum.cpp @@ -132,11 +132,10 @@ TEST_SUBMODULE(native_enum, m) { return static_cast(e); }); - m.def("native_enum_data_missing_finalize_error_message", [](const char *enum_name) { - py::detail::native_enum_data data( - py::none(), enum_name, std::type_index(typeid(void)), false); - return data.missing_finalize_error_message(); - }); + 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 }; From 8b07a748a262a7a0b550a79da091cf086dc367f6 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sun, 9 Mar 2025 22:09:09 -0700 Subject: [PATCH 12/29] [ci skip] Update the Enumerations section in classes.rst --- docs/classes.rst | 93 +++++++++++++++--------------------------------- 1 file changed, 28 insertions(+), 65 deletions(-) diff --git a/docs/classes.rst b/docs/classes.rst index 94e9e7b695..f53fdb5c2e 100644 --- a/docs/classes.rst +++ b/docs/classes.rst @@ -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.: @@ -494,74 +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") + py::native_enum(pet, "Kind") .value("Dog", Pet::Kind::Dog) .value("Cat", Pet::Kind::Cat) - .export_values(); + .export_values() + .finalize(); py::class_(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", 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 `_ for background). - -.. note:: - - ``py::native_enum`` was added as an alternative to ``py::enum_`` - with http://github.com/pybind/pybind11/pull/5555 +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 `_) +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`. From 11f597bb50398c43a0c0a395e8bf401a0407e64e Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sat, 15 Mar 2025 11:54:18 -0700 Subject: [PATCH 13/29] =?UTF-8?q?Rename=20`py::native=5Fenum=5Fkind`=20?= =?UTF-8?q?=E2=86=92=20`py::enum=5Fkind`=20as=20suggested=20by=20gh-henryi?= =?UTF-8?q?ii:?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://github.com/pybind/pybind11/pull/5555#issuecomment-2711672335 --- include/pybind11/native_enum.h | 10 ++++----- tests/test_native_enum.cpp | 38 +++++++++++++++------------------- 2 files changed, 21 insertions(+), 27 deletions(-) diff --git a/include/pybind11/native_enum.h b/include/pybind11/native_enum.h index 6ea4aa51f1..aa89fed767 100644 --- a/include/pybind11/native_enum.h +++ b/include/pybind11/native_enum.h @@ -15,7 +15,7 @@ PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE) -enum class native_enum_kind { Enum, IntEnum }; +enum class enum_kind { Enum, IntEnum }; /// Conversions between Python's native (stdlib) enum types and C++ enums. template @@ -23,11 +23,9 @@ class native_enum : public detail::native_enum_data { public: using Underlying = typename std::underlying_type::type; - explicit native_enum(const object &parent_scope, const char *name, native_enum_kind kind) - : detail::native_enum_data(parent_scope, - name, - std::type_index(typeid(Type)), - kind == native_enum_kind::IntEnum) { + explicit native_enum(const object &parent_scope, const char *name, enum_kind kind) + : detail::native_enum_data( + parent_scope, name, std::type_index(typeid(Type)), kind == enum_kind::IntEnum) { if (detail::get_local_type_info(typeid(Type)) != nullptr || detail::get_global_type_info(typeid(Type)) != nullptr) { pybind11_fail( diff --git a/tests/test_native_enum.cpp b/tests/test_native_enum.cpp index 801d874496..418061ee47 100644 --- a/tests/test_native_enum.cpp +++ b/tests/test_native_enum.cpp @@ -71,31 +71,31 @@ PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE) TEST_SUBMODULE(native_enum, m) { using namespace test_native_enum; - py::native_enum(m, "smallenum", py::native_enum_kind::IntEnum) + py::native_enum(m, "smallenum", py::enum_kind::IntEnum) .value("a", smallenum::a) .value("b", smallenum::b) .value("c", smallenum::c) .finalize(); - py::native_enum(m, "color", py::native_enum_kind::IntEnum) + py::native_enum(m, "color", py::enum_kind::IntEnum) .value("red", color::red) .value("yellow", color::yellow) .value("green", color::green) .value("blue", color::blue) .finalize(); - py::native_enum(m, "altitude", py::native_enum_kind::Enum) + py::native_enum(m, "altitude", py::enum_kind::Enum) .value("high", altitude::high) .value("low", altitude::low) .finalize(); - py::native_enum(m, "export_values", py::native_enum_kind::IntEnum) + py::native_enum(m, "export_values", py::enum_kind::IntEnum) .value("exv0", export_values::exv0) .value("exv1", export_values::exv1) .export_values() .finalize(); - py::native_enum(m, "member_doc", py::native_enum_kind::IntEnum) + py::native_enum(m, "member_doc", py::enum_kind::IntEnum) .value("mem0", member_doc::mem0, "docA") .value("mem1", member_doc::mem1) .value("mem2", member_doc::mem2, "docC") @@ -103,7 +103,7 @@ TEST_SUBMODULE(native_enum, m) { py::class_ py_class_with_enum(m, "class_with_enum"); py::native_enum( - py_class_with_enum, "in_class", py::native_enum_kind::IntEnum) + py_class_with_enum, "in_class", py::enum_kind::IntEnum) .value("one", class_with_enum::in_class::one) .value("two", class_with_enum::in_class::two) .finalize(); @@ -139,13 +139,12 @@ TEST_SUBMODULE(native_enum, m) { m.def("native_enum_ctor_malformed_utf8", [](const char *malformed_utf8) { enum fake { x }; - py::native_enum{py::none(), malformed_utf8, py::native_enum_kind::IntEnum}; + py::native_enum{py::none(), malformed_utf8, py::enum_kind::IntEnum}; }); m.def("native_enum_double_finalize", [](py::module_ &m) { enum fake { x }; - py::native_enum ne( - m, "fake_native_enum_double_finalize", py::native_enum_kind::IntEnum); + py::native_enum ne(m, "fake_native_enum_double_finalize", py::enum_kind::IntEnum); ne.finalize(); ne.finalize(); }); @@ -153,38 +152,35 @@ TEST_SUBMODULE(native_enum, m) { m.def("native_enum_value_after_finalize", [](py::module_ &m) { enum fake { x }; py::native_enum ne( - m, "fake_native_enum_value_after_finalize", py::native_enum_kind::IntEnum); + m, "fake_native_enum_value_after_finalize", py::enum_kind::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(py::none(), "fake", py::native_enum_kind::IntEnum) + py::native_enum(py::none(), "fake", py::enum_kind::IntEnum) .value(malformed_utf8, fake::x); }); m.def("double_registration_native_enum", [](py::module_ &m) { enum fake { x }; - py::native_enum( - m, "fake_double_registration_native_enum", py::native_enum_kind::IntEnum) + py::native_enum(m, "fake_double_registration_native_enum", py::enum_kind::IntEnum) .value("x", fake::x) .finalize(); - py::native_enum( - m, "fake_double_registration_native_enum", py::native_enum_kind::Enum); + py::native_enum(m, "fake_double_registration_native_enum", py::enum_kind::Enum); }); m.def("native_enum_name_clash", [](py::module_ &m) { enum fake { x }; - py::native_enum(m, "fake_native_enum_name_clash", py::native_enum_kind::IntEnum) + py::native_enum(m, "fake_native_enum_name_clash", py::enum_kind::IntEnum) .value("x", fake::x) .finalize(); }); m.def("native_enum_value_name_clash", [](py::module_ &m) { enum fake { x }; - py::native_enum( - m, "fake_native_enum_value_name_clash", py::native_enum_kind::IntEnum) + py::native_enum(m, "fake_native_enum_value_name_clash", py::enum_kind::IntEnum) .value("fake_native_enum_value_name_clash_x", fake::x) .export_values() .finalize(); @@ -193,14 +189,14 @@ TEST_SUBMODULE(native_enum, m) { m.def("double_registration_enum_before_native_enum", [](py::module_ &m) { enum fake { x }; py::enum_(m, "fake_enum_first").value("x", fake::x); - py::native_enum(m, "fake_enum_first", py::native_enum_kind::IntEnum) + py::native_enum(m, "fake_enum_first", py::enum_kind::IntEnum) .value("x", fake::x) .finalize(); }); m.def("double_registration_native_enum_before_enum", [](py::module_ &m) { enum fake { x }; - py::native_enum(m, "fake_native_enum_first", py::native_enum_kind::IntEnum) + py::native_enum(m, "fake_native_enum_first", py::enum_kind::IntEnum) .value("x", fake::x) .finalize(); py::enum_(m, "name_must_be_different_to_reach_desired_code_path"); @@ -210,7 +206,7 @@ TEST_SUBMODULE(native_enum, m) { m.def("native_enum_missing_finalize_failure", []() { enum fake { x }; py::native_enum( - py::none(), "fake_native_enum_missing_finalize_failure", py::native_enum_kind::IntEnum) + py::none(), "fake_native_enum_missing_finalize_failure", py::enum_kind::IntEnum) .value("x", fake::x) // .finalize() missing ; From 190f99c49bb1a7d87361d88cb73ac0e249f8c769 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sat, 15 Mar 2025 13:54:58 -0700 Subject: [PATCH 14/29] 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 = 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) _ = bases = (,) boundary = None class_name = 'greek' classdict = {'_generate_next_value_': , 'Alpha': 10, 'Omega': 20, '__module__': 'test_native_enum'} cls = first_enum = item = ('Omega', 20) member_name = 'Omega' member_value = 20 metacls = 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__ = __new__ = _gnv = )> _order_ = None _simple = False bases = (,) boundary = None classdict = {'Alpha': , 'Omega': , '__module__': 'test_native_enum', '_all_bits_': 0, ...} cls = 'greek' exc = TypeError('10 is not a string') first_enum = ignore = ['_ignore_'] invalid_names = set() key = '_ignore_' kwds = {} member_names = {'Alpha': None, 'Omega': None} member_type = metacls = name = 'Omega' save_new = False tb = use_args = True value = 20 /usr/lib/python3.12/enum.py:596: in __new__ enum_class = super().__new__(metacls, cls, bases, classdict, **kwds) __class__ = __new__ = _gnv = )> _order_ = None _simple = False bases = (,) boundary = None classdict = {'Alpha': , 'Omega': , '__module__': 'test_native_enum', '_all_bits_': 0, ...} cls = 'greek' exc = TypeError('10 is not a string') first_enum = ignore = ['_ignore_'] invalid_names = set() key = '_ignore_' kwds = {} member_names = {'Alpha': None, 'Omega': None} member_type = metacls = name = 'Omega' save_new = False tb = 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 = member_name = 'Alpha' self = value = 10 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ cls = , 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 = 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 ``` --- include/pybind11/detail/native_enum_data.h | 31 +++++++++++++++++----- include/pybind11/native_enum.h | 11 +++++--- tests/test_native_enum.cpp | 12 +++++++++ tests/test_native_enum.py | 5 ++++ 4 files changed, 49 insertions(+), 10 deletions(-) diff --git a/include/pybind11/detail/native_enum_data.h b/include/pybind11/detail/native_enum_data.h index c04624757b..0d7ac8c3be 100644 --- a/include/pybind11/detail/native_enum_data.h +++ b/include/pybind11/detail/native_enum_data.h @@ -15,6 +15,9 @@ #include PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE) + +enum class enum_kind { Enum, IntEnum, StrEnum, COUNT /* Sentinel to trigger static_assert */ }; + PYBIND11_NAMESPACE_BEGIN(detail) // This is a separate function only to enable easy unit testing. @@ -28,10 +31,10 @@ class native_enum_data { native_enum_data(const object &parent_scope, const char *enum_name, const std::type_index &enum_type_index, - bool use_int_enum) + enum_kind kind) : enum_name_encoded{enum_name}, enum_type_index{enum_type_index}, enum_name{enum_name}, - parent_scope(parent_scope), export_values_flag{false}, use_int_enum{use_int_enum}, - finalize_needed{false} {} + parent_scope(parent_scope), export_values_flag{false}, finalize_needed{false}, + kind{kind} {} void finalize(); @@ -71,11 +74,11 @@ class native_enum_data { protected: list members; list docs; - bool export_values_flag : 1; + bool export_values_flag : 1; // Attention: It is best to keep the bools together. private: - bool use_int_enum : 1; bool finalize_needed : 1; + enum_kind kind; }; inline void global_internals_native_enum_type_map_set_item(const std::type_index &enum_type_index, @@ -102,6 +105,22 @@ global_internals_native_enum_type_map_contains(const std::type_index &enum_type_ }); } +inline const char *enum_kind_to_string(enum_kind kind) { + switch (kind) { + case enum_kind::Enum: + return "Enum"; + case enum_kind::IntEnum: + return "IntEnum"; + case enum_kind::StrEnum: + return "StrEnum"; + default: + break; + } + // Ensure that all enum cases are handled at compile time + static_assert(static_cast(enum_kind::COUNT) == 3, "Missing enum cases in switch!"); + pybind11_fail("Unexpected pybind11::enum_kind"); +} + inline void native_enum_data::finalize() { disarm_finalize_check("DOUBLE finalize"); if (hasattr(parent_scope, enum_name)) { @@ -114,7 +133,7 @@ inline void native_enum_data::finalize() { "`import enum` FAILED at " __FILE__ ":" PYBIND11_TOSTRING(__LINE__)); throw error_already_set(); } - auto py_enum_type = enum_module.attr(use_int_enum ? "IntEnum" : "Enum"); + auto py_enum_type = enum_module.attr(enum_kind_to_string(kind)); auto py_enum = py_enum_type(enum_name, members); object module_name = get_module_name_if_available(parent_scope); if (module_name) { diff --git a/include/pybind11/native_enum.h b/include/pybind11/native_enum.h index aa89fed767..cb8c902375 100644 --- a/include/pybind11/native_enum.h +++ b/include/pybind11/native_enum.h @@ -15,8 +15,6 @@ PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE) -enum class enum_kind { Enum, IntEnum }; - /// Conversions between Python's native (stdlib) enum types and C++ enums. template class native_enum : public detail::native_enum_data { @@ -24,8 +22,13 @@ class native_enum : public detail::native_enum_data { using Underlying = typename std::underlying_type::type; explicit native_enum(const object &parent_scope, const char *name, enum_kind kind) - : detail::native_enum_data( - parent_scope, name, std::type_index(typeid(Type)), kind == enum_kind::IntEnum) { + : detail::native_enum_data(parent_scope, name, std::type_index(typeid(Type)), kind) { +#if PY_VERSION_HEX < 0x030B0000 + // Preempt failure downstream, to produce a helpful error message. + if (kind == enum_kind::StrEnum) { + pybind11_fail("pybind11::enum_kind::StrEnum is available only with Python 3.11+"); + } +#endif if (detail::get_local_type_info(typeid(Type)) != nullptr || detail::get_global_type_info(typeid(Type)) != nullptr) { pybind11_fail( diff --git a/tests/test_native_enum.cpp b/tests/test_native_enum.cpp index 418061ee47..f1b82643d7 100644 --- a/tests/test_native_enum.cpp +++ b/tests/test_native_enum.cpp @@ -28,6 +28,8 @@ struct class_with_enum { enum class in_class { one, two }; }; +enum class greek { Alpha = 10, Omega = 20 }; + // https://github.com/protocolbuffers/protobuf/blob/d70b5c5156858132decfdbae0a1103e6a5cb1345/src/google/protobuf/generated_enum_util.h#L52-L53 template struct is_proto_enum : std::false_type {}; @@ -132,6 +134,16 @@ TEST_SUBMODULE(native_enum, m) { return static_cast(e); }); + m.def("native_enum_StrEnum_greek", [](py::module_ &m) { + py::native_enum(m, "greek", py::enum_kind::StrEnum) + .value("Alpha", greek::Alpha) + .value("Omega", greek::Omega) + .finalize(); + }); + + m.def("pass_greek", [](greek e) { return static_cast(e); }); + m.def("return_greek", [](int i) { return static_cast(i); }); + 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); diff --git a/tests/test_native_enum.py b/tests/test_native_enum.py index 1ad51a6fcb..c433a50812 100644 --- a/tests/test_native_enum.py +++ b/tests/test_native_enum.py @@ -145,6 +145,11 @@ def test_py_cast_color_handle(): assert m.py_cast_color_handle(m.color[name]) == value +def test_native_enum_StrEnum_greek(): + assert not hasattr(m, "greek") + m.native_enum_StrEnum_greek(m) + + 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()' From df39090d3804a429e681a8d2e0b96940f51ada88 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sat, 15 Mar 2025 14:00:09 -0700 Subject: [PATCH 15/29] Remove StrEnum code. --- include/pybind11/detail/native_enum_data.h | 6 ++---- include/pybind11/native_enum.h | 6 ------ tests/test_native_enum.cpp | 10 ---------- tests/test_native_enum.py | 5 ----- 4 files changed, 2 insertions(+), 25 deletions(-) diff --git a/include/pybind11/detail/native_enum_data.h b/include/pybind11/detail/native_enum_data.h index 0d7ac8c3be..184056b293 100644 --- a/include/pybind11/detail/native_enum_data.h +++ b/include/pybind11/detail/native_enum_data.h @@ -16,7 +16,7 @@ PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE) -enum class enum_kind { Enum, IntEnum, StrEnum, COUNT /* Sentinel to trigger static_assert */ }; +enum class enum_kind { Enum, IntEnum, COUNT /* Sentinel to trigger static_assert */ }; PYBIND11_NAMESPACE_BEGIN(detail) @@ -111,13 +111,11 @@ inline const char *enum_kind_to_string(enum_kind kind) { return "Enum"; case enum_kind::IntEnum: return "IntEnum"; - case enum_kind::StrEnum: - return "StrEnum"; default: break; } // Ensure that all enum cases are handled at compile time - static_assert(static_cast(enum_kind::COUNT) == 3, "Missing enum cases in switch!"); + static_assert(static_cast(enum_kind::COUNT) == 2, "Missing enum cases in switch!"); pybind11_fail("Unexpected pybind11::enum_kind"); } diff --git a/include/pybind11/native_enum.h b/include/pybind11/native_enum.h index cb8c902375..b6a199ff8b 100644 --- a/include/pybind11/native_enum.h +++ b/include/pybind11/native_enum.h @@ -23,12 +23,6 @@ class native_enum : public detail::native_enum_data { explicit native_enum(const object &parent_scope, const char *name, enum_kind kind) : detail::native_enum_data(parent_scope, name, std::type_index(typeid(Type)), kind) { -#if PY_VERSION_HEX < 0x030B0000 - // Preempt failure downstream, to produce a helpful error message. - if (kind == enum_kind::StrEnum) { - pybind11_fail("pybind11::enum_kind::StrEnum is available only with Python 3.11+"); - } -#endif if (detail::get_local_type_info(typeid(Type)) != nullptr || detail::get_global_type_info(typeid(Type)) != nullptr) { pybind11_fail( diff --git a/tests/test_native_enum.cpp b/tests/test_native_enum.cpp index f1b82643d7..09e94b123e 100644 --- a/tests/test_native_enum.cpp +++ b/tests/test_native_enum.cpp @@ -134,16 +134,6 @@ TEST_SUBMODULE(native_enum, m) { return static_cast(e); }); - m.def("native_enum_StrEnum_greek", [](py::module_ &m) { - py::native_enum(m, "greek", py::enum_kind::StrEnum) - .value("Alpha", greek::Alpha) - .value("Omega", greek::Omega) - .finalize(); - }); - - m.def("pass_greek", [](greek e) { return static_cast(e); }); - m.def("return_greek", [](int i) { return static_cast(i); }); - 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); diff --git a/tests/test_native_enum.py b/tests/test_native_enum.py index c433a50812..1ad51a6fcb 100644 --- a/tests/test_native_enum.py +++ b/tests/test_native_enum.py @@ -145,11 +145,6 @@ def test_py_cast_color_handle(): assert m.py_cast_color_handle(m.color[name]) == value -def test_native_enum_StrEnum_greek(): - assert not hasattr(m, "greek") - m.native_enum_StrEnum_greek(m) - - 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()' From 6180ebc60a683f36e017d50997bbc0eda3b6fc14 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sat, 15 Mar 2025 19:23:22 -0700 Subject: [PATCH 16/29] Make enum_kind::Enum the default kind. --- include/pybind11/native_enum.h | 2 +- tests/test_native_enum.cpp | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/include/pybind11/native_enum.h b/include/pybind11/native_enum.h index b6a199ff8b..6edcf53efb 100644 --- a/include/pybind11/native_enum.h +++ b/include/pybind11/native_enum.h @@ -21,7 +21,7 @@ class native_enum : public detail::native_enum_data { public: using Underlying = typename std::underlying_type::type; - explicit native_enum(const object &parent_scope, const char *name, enum_kind kind) + native_enum(const object &parent_scope, const char *name, enum_kind kind = enum_kind::Enum) : detail::native_enum_data(parent_scope, name, std::type_index(typeid(Type)), kind) { if (detail::get_local_type_info(typeid(Type)) != nullptr || detail::get_global_type_info(typeid(Type)) != nullptr) { diff --git a/tests/test_native_enum.cpp b/tests/test_native_enum.cpp index 09e94b123e..07438f86ee 100644 --- a/tests/test_native_enum.cpp +++ b/tests/test_native_enum.cpp @@ -86,7 +86,7 @@ TEST_SUBMODULE(native_enum, m) { .value("blue", color::blue) .finalize(); - py::native_enum(m, "altitude", py::enum_kind::Enum) + py::native_enum(m, "altitude") .value("high", altitude::high) .value("low", altitude::low) .finalize(); @@ -170,7 +170,7 @@ TEST_SUBMODULE(native_enum, m) { py::native_enum(m, "fake_double_registration_native_enum", py::enum_kind::IntEnum) .value("x", fake::x) .finalize(); - py::native_enum(m, "fake_double_registration_native_enum", py::enum_kind::Enum); + py::native_enum(m, "fake_double_registration_native_enum"); }); m.def("native_enum_name_clash", [](py::module_ &m) { From a0218f4c74204759ee61a9d7f0d2b26aac83fd3b Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sat, 15 Mar 2025 19:29:06 -0700 Subject: [PATCH 17/29] Catch redundant .export_values() calls. --- include/pybind11/detail/native_enum_data.h | 2 +- include/pybind11/native_enum.h | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/include/pybind11/detail/native_enum_data.h b/include/pybind11/detail/native_enum_data.h index 184056b293..94bb3e37e9 100644 --- a/include/pybind11/detail/native_enum_data.h +++ b/include/pybind11/detail/native_enum_data.h @@ -60,7 +60,7 @@ class native_enum_data { } void arm_finalize_check() { - assert(!finalize_needed); + assert(!finalize_needed); // Catch redundant calls. finalize_needed = true; } diff --git a/include/pybind11/native_enum.h b/include/pybind11/native_enum.h index 6edcf53efb..008cc0f256 100644 --- a/include/pybind11/native_enum.h +++ b/include/pybind11/native_enum.h @@ -9,6 +9,7 @@ #include "detail/type_caster_base.h" #include "cast.h" +#include #include #include #include @@ -38,6 +39,7 @@ class native_enum : public detail::native_enum_data { /// Export enumeration entries into the parent scope native_enum &export_values() { + assert(!export_values_flag); // Catch redundant calls. export_values_flag = true; return *this; } From 32e848a33ef95ca73f3ab9f25073216f37a416f1 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sat, 15 Mar 2025 20:16:08 -0700 Subject: [PATCH 18/29] [ci skip] Add back original documentation for `py::enum_` under new advanced/deprecated.rst --- docs/advanced/deprecated.rst | 113 +++++++++++++++++++++++++++++++++++ docs/classes.rst | 8 ++- docs/index.rst | 1 + 3 files changed, 119 insertions(+), 3 deletions(-) create mode 100644 docs/advanced/deprecated.rst diff --git a/docs/advanced/deprecated.rst b/docs/advanced/deprecated.rst new file mode 100644 index 0000000000..a43c447aa5 --- /dev/null +++ b/docs/advanced/deprecated.rst @@ -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 `_ +(see also `#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(m, "Pet"); + + pet.def(py::init()) + .def_readwrite("name", &Pet::name) + .def_readwrite("type", &Pet::type) + .def_readwrite("attr", &Pet::attr); + + py::enum_(pet, "Kind") + .value("Dog", Pet::Kind::Dog) + .value("Cat", Pet::Kind::Cat) + .export_values(); + + py::class_(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", 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 `_ for background). diff --git a/docs/classes.rst b/docs/classes.rst index f53fdb5c2e..43c8874945 100644 --- a/docs/classes.rst +++ b/docs/classes.rst @@ -459,6 +459,8 @@ you can use ``py::detail::overload_cast_impl`` with an additional set of parenth other using the ``.def(py::init<...>())`` syntax. The existing machinery for specifying keyword and default arguments also works. +.. _native_enum: + Enumerations ============ @@ -525,6 +527,6 @@ not PEP 435 compatible 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`. +.. note:: + + The deprecated ``py::enum_`` is documented under :ref:`deprecated_enum`. diff --git a/docs/index.rst b/docs/index.rst index 4e2e8ca3a0..77b097c574 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -36,6 +36,7 @@ advanced/pycpp/index advanced/embedding advanced/misc + advanced/deprecated .. toctree:: :caption: Extra Information From ff25f5a1c89838642074cb4e7983b1c9dd8b9486 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sat, 15 Mar 2025 22:14:21 -0700 Subject: [PATCH 19/29] [ci skip] Add documentation for `py::enum_kind` and `py::detail::type_caster_enum_type_enabled` --- docs/advanced/cast/custom.rst | 2 ++ docs/classes.rst | 61 ++++++++++++++++++++++++++++------- 2 files changed, 52 insertions(+), 11 deletions(-) diff --git a/docs/advanced/cast/custom.rst b/docs/advanced/cast/custom.rst index 5a626f3ba8..1de0f0a21a 100644 --- a/docs/advanced/cast/custom.rst +++ b/docs/advanced/cast/custom.rst @@ -1,3 +1,5 @@ +.. _custom_type_caster: + Custom type casters =================== diff --git a/docs/classes.rst b/docs/classes.rst index 43c8874945..cd5da514e9 100644 --- a/docs/classes.rst +++ b/docs/classes.rst @@ -461,8 +461,8 @@ you can use ``py::detail::overload_cast_impl`` with an additional set of parenth .. _native_enum: -Enumerations -============ +Enumerations and internal types +=============================== Let's now suppose that the example class contains internal types like enumerations, e.g.: @@ -489,6 +489,8 @@ The binding code for this example looks as follows: .. code-block:: cpp + #include // Not already included with pybind11.h + py::class_ pet(m, "Pet"); pet.def(py::init()) @@ -513,20 +515,57 @@ 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. +``py::native_enum`` was introduced with pybind11v3. 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 `_) +but remains supported indefinitely for backward compatibility. +New bindings should prefer ``py::native_enum``. + +.. note:: + + The deprecated ``py::enum_`` is :ref:`documented here `. + 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 +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. -``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 `_) -but remains supported indefinitely for backward compatibility. -New bindings should prefer ``py::native_enum``. +The ``py::native_enum`` constructor supports a third optional ``py::enum_kind`` +argument, with default value ``py::enum_kind::Enum``, which corresponds to +Python's enum.Enum_. The alternative enum.IntEnum_ can be requested like this: + +.. code-block:: cpp + + py::native_enum(pet, "Kind", py::enum_kind::IntEnum) + +Currently, ``py::enum_kind::Enum`` and ``py::enum_kind::IntEnum`` are the only +available options. + +.. _enum.Enum: https://docs.python.org/3/library/enum.html#enum.Enum +.. _enum.IntEnum: https://docs.python.org/3/library/enum.html#enum.IntEnum .. note:: - The deprecated ``py::enum_`` is documented under :ref:`deprecated_enum`. + In rare cases, a C++ enum may be bound to Python via a + :ref:`custom type caster `. In such cases, a + template specialization like this may be required: + + .. code-block:: cpp + + #if defined(PYBIND11_HAS_NATIVE_ENUM) + namespace pybind11::detail { + template + struct type_caster_enum_type_enabled< + FancyEnum, + std::enable_if_t::value>> : std::false_type {}; + } + #endif + + This specialization is needed only if the custom type caster is templated. + + The ``PYBIND11_HAS_NATIVE_ENUM`` guard is needed only if backward + compatibility with pybind11v2 is required. From 1c138660e3881dcd0057c81da50ebed5a6c69cb9 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sun, 16 Mar 2025 10:42:30 -0700 Subject: [PATCH 20/29] Rename `Type` to `EnumType` for readability. --- include/pybind11/native_enum.h | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/include/pybind11/native_enum.h b/include/pybind11/native_enum.h index 008cc0f256..80d89be034 100644 --- a/include/pybind11/native_enum.h +++ b/include/pybind11/native_enum.h @@ -17,15 +17,15 @@ PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE) /// Conversions between Python's native (stdlib) enum types and C++ enums. -template +template class native_enum : public detail::native_enum_data { public: - using Underlying = typename std::underlying_type::type; + using Underlying = typename std::underlying_type::type; native_enum(const object &parent_scope, const char *name, enum_kind kind = enum_kind::Enum) - : detail::native_enum_data(parent_scope, name, std::type_index(typeid(Type)), kind) { - if (detail::get_local_type_info(typeid(Type)) != nullptr - || detail::get_global_type_info(typeid(Type)) != nullptr) { + : detail::native_enum_data(parent_scope, name, std::type_index(typeid(EnumType)), kind) { + 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_`!"); @@ -45,7 +45,7 @@ class native_enum : public detail::native_enum_data { } /// Add an enumeration entry - native_enum &value(char const *name, Type value, const char *doc = nullptr) { + 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(value))); From 8efb9bf81a8d53e5c9b7ec9b9bd690bbdad91d54 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sun, 16 Mar 2025 12:06:51 -0700 Subject: [PATCH 21/29] Eliminate py::enum_kind, use "enum.Enum", "enum.IntEnum" directly. This is still WIP. --- include/pybind11/detail/native_enum_data.h | 54 +++++++++++----------- include/pybind11/native_enum.h | 7 ++- tests/test_native_enum.cpp | 35 ++++++-------- 3 files changed, 46 insertions(+), 50 deletions(-) diff --git a/include/pybind11/detail/native_enum_data.h b/include/pybind11/detail/native_enum_data.h index 94bb3e37e9..c5e6be21b5 100644 --- a/include/pybind11/detail/native_enum_data.h +++ b/include/pybind11/detail/native_enum_data.h @@ -15,9 +15,6 @@ #include PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE) - -enum class enum_kind { Enum, IntEnum, COUNT /* Sentinel to trigger static_assert */ }; - PYBIND11_NAMESPACE_BEGIN(detail) // This is a separate function only to enable easy unit testing. @@ -30,11 +27,11 @@ class native_enum_data { public: native_enum_data(const object &parent_scope, const char *enum_name, - const std::type_index &enum_type_index, - enum_kind kind) - : enum_name_encoded{enum_name}, enum_type_index{enum_type_index}, enum_name{enum_name}, - parent_scope(parent_scope), export_values_flag{false}, finalize_needed{false}, - kind{kind} {} + 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(); @@ -65,11 +62,13 @@ class native_enum_data { } std::string enum_name_encoded; + std::string native_type_name_encoded; std::type_index enum_type_index; private: - str enum_name; object parent_scope; + str enum_name; + str native_type_name; protected: list members; @@ -78,7 +77,6 @@ class native_enum_data { private: bool finalize_needed : 1; - enum_kind kind; }; inline void global_internals_native_enum_type_map_set_item(const std::type_index &enum_type_index, @@ -105,18 +103,24 @@ global_internals_native_enum_type_map_contains(const std::type_index &enum_type_ }); } -inline const char *enum_kind_to_string(enum_kind kind) { - switch (kind) { - case enum_kind::Enum: - return "Enum"; - case enum_kind::IntEnum: - return "IntEnum"; - default: - break; +inline object import_module_get_attr(const std::string &fully_qualified_attribute_name) { + std::size_t last_dot = fully_qualified_attribute_name.rfind('.'); + if (last_dot == std::string::npos) { + throw value_error("WIP-TODO no dot"); + } + std::string module_name = fully_qualified_attribute_name.substr(0, last_dot); + if (module_name.empty()) { + throw value_error("WIP-TODO empty module name"); } - // Ensure that all enum cases are handled at compile time - static_assert(static_cast(enum_kind::COUNT) == 2, "Missing enum cases in switch!"); - pybind11_fail("Unexpected pybind11::enum_kind"); + std::string attr_name = fully_qualified_attribute_name.substr(last_dot + 1); + if (attr_name.empty()) { + throw value_error("WIP-TODO empty attr"); + } + auto py_module = reinterpret_steal(PyImport_ImportModule(module_name.c_str())); + if (!py_module) { + throw error_already_set(); + } + return py_module.attr(attr_name.c_str()); } inline void native_enum_data::finalize() { @@ -125,13 +129,7 @@ inline void native_enum_data::finalize() { pybind11_fail("pybind11::native_enum<...>(\"" + enum_name_encoded + "\"): an object with that name is already defined"); } - auto enum_module = reinterpret_steal(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(enum_kind_to_string(kind)); + auto py_enum_type = import_module_get_attr(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) { diff --git a/include/pybind11/native_enum.h b/include/pybind11/native_enum.h index 80d89be034..759acd48db 100644 --- a/include/pybind11/native_enum.h +++ b/include/pybind11/native_enum.h @@ -22,8 +22,11 @@ class native_enum : public detail::native_enum_data { public: using Underlying = typename std::underlying_type::type; - native_enum(const object &parent_scope, const char *name, enum_kind kind = enum_kind::Enum) - : detail::native_enum_data(parent_scope, name, std::type_index(typeid(EnumType)), kind) { + 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( diff --git a/tests/test_native_enum.cpp b/tests/test_native_enum.cpp index 07438f86ee..279a471ce2 100644 --- a/tests/test_native_enum.cpp +++ b/tests/test_native_enum.cpp @@ -73,13 +73,13 @@ PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE) TEST_SUBMODULE(native_enum, m) { using namespace test_native_enum; - py::native_enum(m, "smallenum", py::enum_kind::IntEnum) + py::native_enum(m, "smallenum", "enum.IntEnum") .value("a", smallenum::a) .value("b", smallenum::b) .value("c", smallenum::c) .finalize(); - py::native_enum(m, "color", py::enum_kind::IntEnum) + py::native_enum(m, "color", "enum.IntEnum") .value("red", color::red) .value("yellow", color::yellow) .value("green", color::green) @@ -91,21 +91,20 @@ TEST_SUBMODULE(native_enum, m) { .value("low", altitude::low) .finalize(); - py::native_enum(m, "export_values", py::enum_kind::IntEnum) + py::native_enum(m, "export_values", "enum.IntEnum") .value("exv0", export_values::exv0) .value("exv1", export_values::exv1) .export_values() .finalize(); - py::native_enum(m, "member_doc", py::enum_kind::IntEnum) + py::native_enum(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_ py_class_with_enum(m, "class_with_enum"); - py::native_enum( - py_class_with_enum, "in_class", py::enum_kind::IntEnum) + py::native_enum(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(); @@ -141,33 +140,31 @@ TEST_SUBMODULE(native_enum, m) { m.def("native_enum_ctor_malformed_utf8", [](const char *malformed_utf8) { enum fake { x }; - py::native_enum{py::none(), malformed_utf8, py::enum_kind::IntEnum}; + py::native_enum{py::none(), malformed_utf8, "enum.IntEnum"}; }); m.def("native_enum_double_finalize", [](py::module_ &m) { enum fake { x }; - py::native_enum ne(m, "fake_native_enum_double_finalize", py::enum_kind::IntEnum); + py::native_enum 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 ne( - m, "fake_native_enum_value_after_finalize", py::enum_kind::IntEnum); + py::native_enum 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(py::none(), "fake", py::enum_kind::IntEnum) - .value(malformed_utf8, fake::x); + py::native_enum(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(m, "fake_double_registration_native_enum", py::enum_kind::IntEnum) + py::native_enum(m, "fake_double_registration_native_enum", "enum.IntEnum") .value("x", fake::x) .finalize(); py::native_enum(m, "fake_double_registration_native_enum"); @@ -175,14 +172,14 @@ TEST_SUBMODULE(native_enum, m) { m.def("native_enum_name_clash", [](py::module_ &m) { enum fake { x }; - py::native_enum(m, "fake_native_enum_name_clash", py::enum_kind::IntEnum) + py::native_enum(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(m, "fake_native_enum_value_name_clash", py::enum_kind::IntEnum) + py::native_enum(m, "fake_native_enum_value_name_clash", "enum.IntEnum") .value("fake_native_enum_value_name_clash_x", fake::x) .export_values() .finalize(); @@ -191,14 +188,12 @@ TEST_SUBMODULE(native_enum, m) { m.def("double_registration_enum_before_native_enum", [](py::module_ &m) { enum fake { x }; py::enum_(m, "fake_enum_first").value("x", fake::x); - py::native_enum(m, "fake_enum_first", py::enum_kind::IntEnum) - .value("x", fake::x) - .finalize(); + py::native_enum(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(m, "fake_native_enum_first", py::enum_kind::IntEnum) + py::native_enum(m, "fake_native_enum_first", "enum.IntEnum") .value("x", fake::x) .finalize(); py::enum_(m, "name_must_be_different_to_reach_desired_code_path"); @@ -208,7 +203,7 @@ TEST_SUBMODULE(native_enum, m) { m.def("native_enum_missing_finalize_failure", []() { enum fake { x }; py::native_enum( - py::none(), "fake_native_enum_missing_finalize_failure", py::enum_kind::IntEnum) + py::none(), "fake_native_enum_missing_finalize_failure", "enum.IntEnum") .value("x", fake::x) // .finalize() missing ; From 9483e8cbc0ca9038f1c0689ee237b9a256189804 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sat, 22 Mar 2025 12:10:15 -0700 Subject: [PATCH 22/29] EXPERIMENTAL StrEnum code. To be removed. --- tests/test_native_enum.cpp | 10 ++++++++++ tests/test_native_enum.py | 14 ++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/tests/test_native_enum.cpp b/tests/test_native_enum.cpp index 279a471ce2..4f23f1e295 100644 --- a/tests/test_native_enum.cpp +++ b/tests/test_native_enum.cpp @@ -133,6 +133,16 @@ TEST_SUBMODULE(native_enum, m) { return static_cast(e); }); + m.def("native_enum_StrEnum_greek", [](py::module_ &m) { + py::native_enum(m, "greek", "enum.StrEnum") + .value("Alpha", greek::Alpha) + .value("Omega", greek::Omega) + .finalize(); + }); + + m.def("pass_greek", [](greek e) { return static_cast(e); }); + m.def("return_greek", [](int i) { return static_cast(i); }); + 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); diff --git a/tests/test_native_enum.py b/tests/test_native_enum.py index 1ad51a6fcb..0dc9c8f739 100644 --- a/tests/test_native_enum.py +++ b/tests/test_native_enum.py @@ -145,6 +145,20 @@ def test_py_cast_color_handle(): assert m.py_cast_color_handle(m.color[name]) == value +def test_native_enum_StrEnum_greek(): + # assert not hasattr(m, "greek") + # m.native_enum_StrEnum_greek(m) + Colors = enum.StrEnum("Colors", (("RED", "r"), ("Green", "g"))) + print("\nLOOOK") + print(f"{Colors("r")=}") + print(f"{Colors.RED=}") + print(f"{str(Colors.RED)=}") + BadColors = enum.StrEnum("BadColors", (("RED", "r"), ("Green", "r"))) + print(f"{BadColors("r")=}") + print(f"{BadColors(BadColors.Green)=}") + Colors._pybind11_cpp_underlying = dict((("RED", 0), ("Green", 1))) # noqa: C406 + + 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()' From 0e46fa08d0427241647949b7248f4e0b7c7813bf Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sat, 22 Mar 2025 12:11:52 -0700 Subject: [PATCH 23/29] Remove experimental StrEnum code: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- tests/test_native_enum.cpp | 12 ------------ tests/test_native_enum.py | 14 -------------- 2 files changed, 26 deletions(-) diff --git a/tests/test_native_enum.cpp b/tests/test_native_enum.cpp index 4f23f1e295..9d3eb1834d 100644 --- a/tests/test_native_enum.cpp +++ b/tests/test_native_enum.cpp @@ -28,8 +28,6 @@ struct class_with_enum { enum class in_class { one, two }; }; -enum class greek { Alpha = 10, Omega = 20 }; - // https://github.com/protocolbuffers/protobuf/blob/d70b5c5156858132decfdbae0a1103e6a5cb1345/src/google/protobuf/generated_enum_util.h#L52-L53 template struct is_proto_enum : std::false_type {}; @@ -133,16 +131,6 @@ TEST_SUBMODULE(native_enum, m) { return static_cast(e); }); - m.def("native_enum_StrEnum_greek", [](py::module_ &m) { - py::native_enum(m, "greek", "enum.StrEnum") - .value("Alpha", greek::Alpha) - .value("Omega", greek::Omega) - .finalize(); - }); - - m.def("pass_greek", [](greek e) { return static_cast(e); }); - m.def("return_greek", [](int i) { return static_cast(i); }); - 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); diff --git a/tests/test_native_enum.py b/tests/test_native_enum.py index 0dc9c8f739..1ad51a6fcb 100644 --- a/tests/test_native_enum.py +++ b/tests/test_native_enum.py @@ -145,20 +145,6 @@ def test_py_cast_color_handle(): assert m.py_cast_color_handle(m.color[name]) == value -def test_native_enum_StrEnum_greek(): - # assert not hasattr(m, "greek") - # m.native_enum_StrEnum_greek(m) - Colors = enum.StrEnum("Colors", (("RED", "r"), ("Green", "g"))) - print("\nLOOOK") - print(f"{Colors("r")=}") - print(f"{Colors.RED=}") - print(f"{str(Colors.RED)=}") - BadColors = enum.StrEnum("BadColors", (("RED", "r"), ("Green", "r"))) - print(f"{BadColors("r")=}") - print(f"{BadColors(BadColors.Green)=}") - Colors._pybind11_cpp_underlying = dict((("RED", 0), ("Green", 1))) # noqa: C406 - - 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()' From 346b47fe02ee283afd08f47435c944242daba892 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sat, 22 Mar 2025 12:43:15 -0700 Subject: [PATCH 24/29] Add test with enum.IntFlag (no production code changes required). --- tests/test_native_enum.cpp | 8 ++++++++ tests/test_native_enum.py | 14 ++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/tests/test_native_enum.cpp b/tests/test_native_enum.cpp index 9d3eb1834d..1b54ad1cc4 100644 --- a/tests/test_native_enum.cpp +++ b/tests/test_native_enum.cpp @@ -20,6 +20,8 @@ enum class altitude : char { low = 'l', // trailing comma only allowed after CWG518 }; +enum class combinable { trait1 = 0x1, trait2 = 0x2, trait3 = 0x4 }; + enum class export_values { exv0, exv1 }; enum class member_doc { mem0, mem1, mem2 }; @@ -89,6 +91,12 @@ TEST_SUBMODULE(native_enum, m) { .value("low", altitude::low) .finalize(); + py::native_enum(m, "combinable", "enum.IntFlag") + .value("trait1", combinable::trait1) + .value("trait2", combinable::trait2) + .value("trait3", combinable::trait3) + .finalize(); + py::native_enum(m, "export_values", "enum.IntEnum") .value("exv0", export_values::exv0) .value("exv1", export_values::exv1) diff --git a/tests/test_native_enum.py b/tests/test_native_enum.py index 1ad51a6fcb..b6b33a86a2 100644 --- a/tests/test_native_enum.py +++ b/tests/test_native_enum.py @@ -26,6 +26,12 @@ ("low", "l"), ) +COMBINABLE_MEMBERS = ( + ("trait1", 0x1), + ("trait2", 0x2), + ("trait3", 0x4), +) + CLASS_WITH_ENUM_IN_CLASS_MEMBERS = ( ("one", 0), ("two", 1), @@ -46,6 +52,7 @@ (m.smallenum, SMALLENUM_MEMBERS), (m.color, COLOR_MEMBERS), (m.altitude, ALTITUDE_MEMBERS), + (m.combinable, COMBINABLE_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), @@ -81,6 +88,13 @@ def test_pickle_roundtrip(enum_type, members): assert restored == orig +def test_enum_intflag(): + traits13 = m.combinable.trait1 | m.combinable.trait3 + assert m.combinable.trait1 in traits13 + assert m.combinable.trait2 not in traits13 + assert m.combinable.trait3 in traits13 + + def test_export_values(): assert m.exv0 is m.export_values.exv0 assert m.exv1 is m.export_values.exv1 From 050febca4fb839da9a2a3bfc3f709a9c3c4ceaf3 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sat, 22 Mar 2025 14:28:05 -0700 Subject: [PATCH 25/29] First import_or_getattr() implementation (dedicated tests are still missing). --- include/pybind11/detail/native_enum_data.h | 59 ++++++++++++++++------ 1 file changed, 43 insertions(+), 16 deletions(-) diff --git a/include/pybind11/detail/native_enum_data.h b/include/pybind11/detail/native_enum_data.h index c5e6be21b5..dbd071fd09 100644 --- a/include/pybind11/detail/native_enum_data.h +++ b/include/pybind11/detail/native_enum_data.h @@ -11,6 +11,7 @@ #include "internals.h" #include +#include #include #include @@ -103,24 +104,50 @@ global_internals_native_enum_type_map_contains(const std::type_index &enum_type_ }); } -inline object import_module_get_attr(const std::string &fully_qualified_attribute_name) { - std::size_t last_dot = fully_qualified_attribute_name.rfind('.'); - if (last_dot == std::string::npos) { - throw value_error("WIP-TODO no dot"); - } - std::string module_name = fully_qualified_attribute_name.substr(0, last_dot); - if (module_name.empty()) { - throw value_error("WIP-TODO empty module name"); - } - std::string attr_name = fully_qualified_attribute_name.substr(last_dot + 1); - if (attr_name.empty()) { - throw value_error("WIP-TODO empty attr"); +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()) { + throw value_error("Invalid fully-qualified name" + append_to_exception_message); } - auto py_module = reinterpret_steal(PyImport_ImportModule(module_name.c_str())); - if (!py_module) { + + auto current = reinterpret_steal(PyImport_ImportModule(part.c_str())); + if (!current) { + raise_from( + PyExc_ImportError, + ("Failed to import top-level module `" + part + "`" + append_to_exception_message) + .c_str()); throw error_already_set(); } - return py_module.attr(attr_name.c_str()); + + // Now recursively getattr or import remaining parts + std::string current_path = part; + while (std::getline(stream, part, '.')) { + if (part.empty()) { + throw value_error("Invalid fully-qualified name" + append_to_exception_message); + } + std::string next_path = current_path + "." + part; + auto next = reinterpret_steal(PyObject_GetAttrString(current.ptr(), part.c_str())); + if (!next) { + error_fetch_and_normalize getattr_error("getattr"); + // Try importing the next level + next = reinterpret_steal(PyImport_ImportModule(next_path.c_str())); + if (!next) { + error_fetch_and_normalize import_error("import"); + std::string msg = ("Failed to import or getattr `" + part + "` from `" + + current_path + "`" + append_to_exception_message + + "\n--------\n" + getattr_error.error_string() + "\n--------\n" + + import_error.error_string()) + .c_str(); + throw error_already_set(); + } + } + current = next; + current_path = next_path; + } + return current; } inline void native_enum_data::finalize() { @@ -129,7 +156,7 @@ inline void native_enum_data::finalize() { pybind11_fail("pybind11::native_enum<...>(\"" + enum_name_encoded + "\"): an object with that name is already defined"); } - auto py_enum_type = import_module_get_attr(native_type_name); + 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) { From f7736262b0b0fe80c1b0e5865f007ab961b26c67 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sat, 22 Mar 2025 20:17:11 -0700 Subject: [PATCH 26/29] Fix import_or_getattr() implementation, add tests, fix clang-tidy errors. --- include/pybind11/detail/native_enum_data.h | 67 ++++++++++++++-------- tests/test_native_enum.cpp | 7 +++ tests/test_native_enum.py | 38 ++++++++++++ 3 files changed, 87 insertions(+), 25 deletions(-) diff --git a/include/pybind11/detail/native_enum_data.h b/include/pybind11/detail/native_enum_data.h index dbd071fd09..5192ed51a6 100644 --- a/include/pybind11/detail/native_enum_data.h +++ b/include/pybind11/detail/native_enum_data.h @@ -110,44 +110,61 @@ inline object import_or_getattr(const std::string &fully_qualified_name, std::string part; if (!std::getline(stream, part, '.') || part.empty()) { - throw value_error("Invalid fully-qualified name" + append_to_exception_message); + std::string msg = "Invalid fully-qualified name `"; + msg += fully_qualified_name; + msg += "`"; + msg += append_to_exception_message; + throw value_error(msg); } - auto current = reinterpret_steal(PyImport_ImportModule(part.c_str())); - if (!current) { - raise_from( - PyExc_ImportError, - ("Failed to import top-level module `" + part + "`" + append_to_exception_message) - .c_str()); + auto curr_scope = reinterpret_steal(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 current_path = part; + std::string curr_path = part; while (std::getline(stream, part, '.')) { if (part.empty()) { - throw value_error("Invalid fully-qualified name" + append_to_exception_message); + 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 = current_path + "." + part; - auto next = reinterpret_steal(PyObject_GetAttrString(current.ptr(), part.c_str())); - if (!next) { - error_fetch_and_normalize getattr_error("getattr"); + std::string next_path = curr_path; + next_path += "."; + next_path += part; + auto next_scope + = reinterpret_steal(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 = reinterpret_steal(PyImport_ImportModule(next_path.c_str())); - if (!next) { - error_fetch_and_normalize import_error("import"); - std::string msg = ("Failed to import or getattr `" + part + "` from `" - + current_path + "`" + append_to_exception_message - + "\n--------\n" + getattr_error.error_string() + "\n--------\n" - + import_error.error_string()) - .c_str(); - throw error_already_set(); + next_scope = reinterpret_steal(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()); } } - current = next; - current_path = next_path; + curr_scope = next_scope; + curr_path = next_path; } - return current; + return curr_scope; } inline void native_enum_data::finalize() { diff --git a/tests/test_native_enum.cpp b/tests/test_native_enum.cpp index 1b54ad1cc4..0b09913c91 100644 --- a/tests/test_native_enum.cpp +++ b/tests/test_native_enum.cpp @@ -139,6 +139,13 @@ TEST_SUBMODULE(native_enum, m) { return static_cast(e); }); + m.def("exercise_import_or_getattr", [](py::module_ &m, const char *native_type_name) { + enum fake { x }; + py::native_enum(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); diff --git a/tests/test_native_enum.py b/tests/test_native_enum.py index b6b33a86a2..96b1ff38f3 100644 --- a/tests/test_native_enum.py +++ b/tests/test_native_enum.py @@ -159,6 +159,44 @@ def test_py_cast_color_handle(): 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()' From 29f143a9e5c982bd21f540ab430aa9b01cf10a4b Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sat, 22 Mar 2025 23:49:54 -0700 Subject: [PATCH 27/29] [ci skip] Update classes.rst: replace `py::enum_kind` with `native_type_name` --- docs/classes.rst | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/docs/classes.rst b/docs/classes.rst index cd5da514e9..894f45b6c6 100644 --- a/docs/classes.rst +++ b/docs/classes.rst @@ -516,8 +516,8 @@ supplied to the ``py::native_enum`` and ``py::class_`` constructors. The into the parent scope, if desired. ``py::native_enum`` was introduced with pybind11v3. It binds C++ enum types -to Python's native enum.Enum_, -making them `PEP 435 compatible `_. +to native Python enum types, typically types in Python's `stdlib enum`_ module, +which are `PEP 435 compatible `_. This is the recommended way to bind C++ enums. The older ``py::enum_`` is not PEP 435 compatible (see `issue #2332 `_) @@ -534,19 +534,33 @@ 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. -The ``py::native_enum`` constructor supports a third optional ``py::enum_kind`` -argument, with default value ``py::enum_kind::Enum``, which corresponds to -Python's enum.Enum_. The alternative enum.IntEnum_ can be requested like this: +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", py::enum_kind::IntEnum) + py::native_enum(pet, "Kind", "enum.IntEnum") -Currently, ``py::enum_kind::Enum`` and ``py::enum_kind::IntEnum`` are the only -available options. +Any fully-qualified Python name can be specified. The only requirement is +that the named type is similar to enum.Enum_ in these ways: +* Has a constructor similar to that of enum.Enum_:: + + Colors = enum.Enum("Colors", (("Red", 0), ("Green", 1))) + +* A `C++ underlying `_ + 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 + +.. _`stdlib enum`: https://docs.python.org/3/library/enum.html .. _enum.Enum: https://docs.python.org/3/library/enum.html#enum.Enum -.. _enum.IntEnum: https://docs.python.org/3/library/enum.html#enum.IntEnum .. note:: From cbb7494549d8c648c0f1d4394972fd2a2b909a37 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sun, 23 Mar 2025 21:04:17 -0700 Subject: [PATCH 28/29] 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). --- docs/classes.rst | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/classes.rst b/docs/classes.rst index 894f45b6c6..1dda27a9f3 100644 --- a/docs/classes.rst +++ b/docs/classes.rst @@ -516,7 +516,8 @@ supplied to the ``py::native_enum`` and ``py::class_`` constructors. The into the parent scope, if desired. ``py::native_enum`` was introduced with pybind11v3. It binds C++ enum types -to native Python enum types, typically types in Python's `stdlib enum`_ module, +to native Python enum types, typically types in Python's +`stdlib enum `_ module, which are `PEP 435 compatible `_. This is the recommended way to bind C++ enums. The older ``py::enum_`` is not PEP 435 compatible @@ -543,9 +544,12 @@ Other types can be specified like this: py::native_enum(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_ in these ways: +that the named type is similar to +`enum.Enum `_ +in these ways: -* Has a constructor similar to that of enum.Enum_:: +* Has a `constructor similar to that of enum.Enum + `_:: Colors = enum.Enum("Colors", (("Red", 0), ("Green", 1))) @@ -559,9 +563,6 @@ that the named type is similar to enum.Enum_ in these ways: underlying = red.value -.. _`stdlib enum`: https://docs.python.org/3/library/enum.html -.. _enum.Enum: https://docs.python.org/3/library/enum.html#enum.Enum - .. note:: In rare cases, a C++ enum may be bound to Python via a From ae07bd261be88fe83975e75fb7cdc36e215eb409 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sun, 23 Mar 2025 21:16:19 -0700 Subject: [PATCH 29/29] 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. --- docs/classes.rst | 4 ++++ tests/test_native_enum.cpp | 17 ++++++++++++----- tests/test_native_enum.py | 28 ++++++++++++++++++---------- 3 files changed, 34 insertions(+), 15 deletions(-) diff --git a/docs/classes.rst b/docs/classes.rst index 1dda27a9f3..e68b802ee9 100644 --- a/docs/classes.rst +++ b/docs/classes.rst @@ -563,6 +563,10 @@ in these ways: underlying = red.value +As of Python 3.13, the compatible `types in the stdlib enum module +`_ are: +``Enum``, ``IntEnum``, ``Flag``, ``IntFlag``. + .. note:: In rare cases, a C++ enum may be bound to Python via a diff --git a/tests/test_native_enum.cpp b/tests/test_native_enum.cpp index 0b09913c91..1a4c3180c7 100644 --- a/tests/test_native_enum.cpp +++ b/tests/test_native_enum.cpp @@ -20,7 +20,8 @@ enum class altitude : char { low = 'l', // trailing comma only allowed after CWG518 }; -enum class combinable { trait1 = 0x1, trait2 = 0x2, trait3 = 0x4 }; +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 }; @@ -91,10 +92,16 @@ TEST_SUBMODULE(native_enum, m) { .value("low", altitude::low) .finalize(); - py::native_enum(m, "combinable", "enum.IntFlag") - .value("trait1", combinable::trait1) - .value("trait2", combinable::trait2) - .value("trait3", combinable::trait3) + py::native_enum(m, "flags_uchar", "enum.Flag") + .value("bit0", flags_uchar::bit0) + .value("bit1", flags_uchar::bit1) + .value("bit2", flags_uchar::bit2) + .finalize(); + + py::native_enum(m, "flags_uint", "enum.IntFlag") + .value("bit0", flags_uint::bit0) + .value("bit1", flags_uint::bit1) + .value("bit2", flags_uint::bit2) .finalize(); py::native_enum(m, "export_values", "enum.IntEnum") diff --git a/tests/test_native_enum.py b/tests/test_native_enum.py index 96b1ff38f3..f98ebd0459 100644 --- a/tests/test_native_enum.py +++ b/tests/test_native_enum.py @@ -26,10 +26,16 @@ ("low", "l"), ) -COMBINABLE_MEMBERS = ( - ("trait1", 0x1), - ("trait2", 0x2), - ("trait3", 0x4), +FLAGS_UCHAR_MEMBERS = ( + ("bit0", 0x1), + ("bit1", 0x2), + ("bit2", 0x4), +) + +FLAGS_UINT_MEMBERS = ( + ("bit0", 0x1), + ("bit1", 0x2), + ("bit2", 0x4), ) CLASS_WITH_ENUM_IN_CLASS_MEMBERS = ( @@ -52,7 +58,8 @@ (m.smallenum, SMALLENUM_MEMBERS), (m.color, COLOR_MEMBERS), (m.altitude, ALTITUDE_MEMBERS), - (m.combinable, COMBINABLE_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), @@ -88,11 +95,12 @@ def test_pickle_roundtrip(enum_type, members): assert restored == orig -def test_enum_intflag(): - traits13 = m.combinable.trait1 | m.combinable.trait3 - assert m.combinable.trait1 in traits13 - assert m.combinable.trait2 not in traits13 - assert m.combinable.trait3 in traits13 +@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():