diff --git a/CMakeLists.txt b/CMakeLists.txt index bf331b67973..c3f5207b385 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -119,6 +119,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/smart_holder_poc.h include/pybind11/detail/smart_holder_sfinae_hooks_only.h include/pybind11/detail/smart_holder_type_casters.h @@ -141,6 +142,7 @@ set(PYBIND11_HEADERS include/pybind11/gil.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 c0c53135b8d..e4901117a9f 100644 --- a/docs/classes.rst +++ b/docs/classes.rst @@ -539,3 +539,8 @@ The ``name`` property returns the name of the enum value as a unicode string. ... By default, these are omitted to conserve space. + +.. note:: + + ``py::native_enum`` was added as an alternative to ``py::enum_`` + with http://github.com/google/pywrapcc/pull/30005 diff --git a/include/pybind11/cast.h b/include/pybind11/cast.h index e8139da20e8..1fb038b598d 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/smart_holder_sfinae_hooks_only.h" #include "detail/type_caster_base.h" #include "detail/type_caster_odr_guard.h" @@ -63,12 +64,6 @@ using make_caster_for_intrinsic = type_caster; template using make_caster = make_caster_for_intrinsic>; -template -struct type_uses_smart_holder_type_caster { - static constexpr bool value - = std::is_base_of>::value; -}; - // Shortcut for calling a caster's `cast_op_type` cast operator for casting a type_caster to a T template typename make_caster::template cast_op_type cast_op(make_caster &caster) { @@ -81,6 +76,126 @@ cast_op(make_caster &&caster) { template cast_op_type::type>(); } +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) { + auto const &natives = cross_extension_shared_states::native_enum_type_map::get(); + auto found = natives.find(std::type_index(typeid(EnumType))); + if (found != natives.end()) { + return handle(found->second)(static_cast(src)).release(); + } + return type_caster_for_class_::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) { + auto const &natives = cross_extension_shared_states::native_enum_type_map::get(); + auto found = natives.find(std::type_index(typeid(EnumType))); + if (found != natives.end()) { + if (!isinstance(src, found->second)) { + 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_for_class_()); + } + 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 +struct type_uses_smart_holder_type_caster { + static constexpr bool value + = std::is_base_of>::value +#ifdef PYBIND11_USE_SMART_HOLDER_AS_DEFAULT + || type_uses_type_caster_enum_type::value +#endif + ; +}; + +template +struct type_caster_classh_enum_aware : type_caster {}; + +#ifdef PYBIND11_USE_SMART_HOLDER_AS_DEFAULT +template +struct type_caster_classh_enum_aware< + EnumType, + detail::enable_if_t::value>> + : type_caster_for_class_ {}; +#endif + +template ::value, int> = 0> +bool isinstance_native_enum_impl(handle obj, const std::type_info &tp) { + auto const &natives = cross_extension_shared_states::native_enum_type_map::get(); + auto found = natives.find(tp); + if (found == natives.end()) { + return false; + } + return isinstance(obj, found->second); +} + +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: @@ -1024,11 +1139,11 @@ using move_never = none_of, move_if_unreferenced>; // non-reference/pointer `type`s and reference/pointers from a type_caster_generic are safe; // everything else returns a reference/pointer to a local variable. template -using cast_is_temporary_value_reference - = bool_constant<(std::is_reference::value || std::is_pointer::value) - && !std::is_base_of>::value - && !type_uses_smart_holder_type_caster>::value - && !std::is_same, void>::value>; +using cast_is_temporary_value_reference = bool_constant< + (std::is_reference::value || std::is_pointer::value) + && !std::is_base_of>::value + && !std::is_base_of>::value + && !std::is_same, void>::value>; // When a value returned from a C++ function is being cast back to Python, we almost always want to // force `policy = move`, regardless of the return value policy the function/method was declared @@ -1086,8 +1201,18 @@ PYBIND11_NAMESPACE_END(detail) template ::value, int> = 0> 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 (cross_extension_shared_states::native_enum_type_map::get().count( + std::type_index(typeid(intrinsic_t))) + != 0) { + pybind11_fail("Unable to cast native enum type to reference"); + } + } +#endif return cast_op(load_type(handle)); } diff --git a/include/pybind11/detail/native_enum_data.h b/include/pybind11/detail/native_enum_data.h new file mode 100644 index 00000000000..b338ea2bcdc --- /dev/null +++ b/include/pybind11/detail/native_enum_data.h @@ -0,0 +1,129 @@ +// Copyright (c) 2022 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 "../pytypes.h" +#include "abi_platform_id.h" +#include "common.h" +#include "cross_extension_shared_state.h" +#include "type_map.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; +}; + +PYBIND11_NAMESPACE_END(detail) + +PYBIND11_NAMESPACE_BEGIN(cross_extension_shared_states) + +struct native_enum_type_map_v1_adapter { + static constexpr const char *abi_id() { + return "__pybind11_native_enum_type_map_v1" PYBIND11_PLATFORM_ABI_ID_V4 "__"; + } + + using payload_type = detail::type_map; + + static void payload_clear(payload_type &payload) { + for (auto it : payload) { + Py_DECREF(it.second); + } + payload.clear(); + } +}; + +using native_enum_type_map_v1 + = detail::cross_extension_shared_state; +using native_enum_type_map = native_enum_type_map_v1; + +PYBIND11_NAMESPACE_END(cross_extension_shared_states) + +PYBIND11_NAMESPACE_BEGIN(detail) + +inline void native_enum_add_to_parent(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__)); + } + auto py_enum_type = enum_module.attr(data.use_int_enum ? "IntEnum" : "Enum"); + auto py_enum = py_enum_type(data.enum_name, data.members); + if (hasattr(parent, "__module__")) { + // Enum nested in class: + py_enum.attr("__module__") = parent.attr("__module__"); + } else { + py_enum.attr("__module__") = parent; + } + 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)]; + } + cross_extension_shared_states::native_enum_type_map::get()[data.enum_type_index] + = py_enum.release().ptr(); +} + +PYBIND11_NAMESPACE_END(detail) + +PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE) diff --git a/include/pybind11/detail/smart_holder_sfinae_hooks_only.h b/include/pybind11/detail/smart_holder_sfinae_hooks_only.h index ca57cb61cb9..5114607fb04 100644 --- a/include/pybind11/detail/smart_holder_sfinae_hooks_only.h +++ b/include/pybind11/detail/smart_holder_sfinae_hooks_only.h @@ -21,6 +21,13 @@ PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE) PYBIND11_NAMESPACE_BEGIN(detail) +constexpr bool smart_holder_is_default_holder_type = +#ifdef PYBIND11_USE_SMART_HOLDER_AS_DEFAULT + true; +#else + false; +#endif + template struct is_smart_holder_type : std::false_type {}; diff --git a/include/pybind11/embed.h b/include/pybind11/embed.h index caa14f4a051..9c8530e3cf8 100644 --- a/include/pybind11/embed.h +++ b/include/pybind11/embed.h @@ -243,6 +243,8 @@ inline void initialize_interpreter(bool init_signal_handlers = true, \endrst */ inline void finalize_interpreter() { + cross_extension_shared_states::native_enum_type_map::scoped_clear native_enum_type_map_clear; + // Get the internals pointer (without creating it if it doesn't exist). It's possible for the // internals to be created during Py_Finalize() (e.g. if a py::capsule calls `get_internals()` // during destruction), so we get the pointer-pointer here and check it after Py_Finalize(). diff --git a/include/pybind11/native_enum.h b/include/pybind11/native_enum.h new file mode 100644 index 00000000000..13c15ab9058 --- /dev/null +++ b/include/pybind11/native_enum.h @@ -0,0 +1,64 @@ +// Copyright (c) 2022 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) + +/// 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) + : detail::native_enum_data(name, + std::type_index(typeid(Type)), + std::numeric_limits::is_integer + && !std::is_same::value + && !detail::is_std_char_type::value) { + 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 (cross_extension_shared_states::native_enum_type_map::get().count(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 eef03cf74fe..ed2e55dbb82 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -12,6 +12,7 @@ #include "detail/class.h" #include "detail/init.h" +#include "detail/native_enum_data.h" #include "detail/smart_holder_sfinae_hooks_only.h" #include "attr.h" #include "gil.h" @@ -1268,6 +1269,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; + } }; // When inside a namespace (or anywhere as long as it's not the first item on a line), @@ -1742,11 +1748,17 @@ class class_ : public detail::generic_type { " missing PYBIND11_SMART_HOLDER_TYPE_CASTERS(T)?"); #ifdef PYBIND11_STRICT_ASSERTS_CLASS_HOLDER_VS_TYPE_CASTER_MIX // Strict conditions cannot be enforced universally at the moment (PR #2836). - static_assert(holder_is_smart_holder == wrapped_type_uses_smart_holder_type_caster, + static_assert(holder_is_smart_holder + == (wrapped_type_uses_smart_holder_type_caster + || (detail::type_uses_type_caster_enum_type::value + && detail::smart_holder_is_default_holder_type)), "py::class_ holder vs type_caster mismatch:" " missing PYBIND11_SMART_HOLDER_TYPE_CASTERS(T)" " or collision with custom py::detail::type_caster?"); - static_assert(!holder_is_smart_holder == type_caster_type_is_type_caster_base_subtype, + static_assert(!holder_is_smart_holder + == (type_caster_type_is_type_caster_base_subtype + || (detail::type_uses_type_caster_enum_type::value + && !detail::smart_holder_is_default_holder_type)), "py::class_ holder vs type_caster mismatch:" " missing PYBIND11_TYPE_CASTER_BASE_HOLDER(T, ...)" " or collision with custom py::detail::type_caster?"); @@ -2030,6 +2042,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: template ::value, int> = 0> @@ -2040,7 +2057,8 @@ class class_ : public detail::generic_type { template ::value, int> = 0> void generic_type_initialize(const detail::type_record &record) { - generic_type::initialize(record, detail::type_caster::get_local_load_function_ptr()); + generic_type::initialize( + record, detail::type_caster_classh_enum_aware::get_local_load_function_ptr()); } /// Initialize holder object, variant 1: object derives from enable_shared_from_this @@ -2111,7 +2129,8 @@ class class_ : public detail::generic_type { typename A = type_alias, detail::enable_if_t::value, int> = 0> static void init_instance(detail::instance *inst, const void *holder_ptr) { - detail::type_caster::template init_instance_for_type(inst, holder_ptr); + detail::type_caster_classh_enum_aware::template init_instance_for_type( + inst, holder_ptr); } /// Deallocates an instance; via holder, if constructed; otherwise via operator delete. @@ -2431,6 +2450,15 @@ class enum_ : public class_ { template enum_(const handle &scope, const char *name, const Extra &...extra) : class_(scope, name, extra...), m_base(*this, scope) { + { + if (cross_extension_shared_states::native_enum_type_map::get().count( + std::type_index(typeid(Type))) + != 0) { + 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 f11ed5da78a..26143496e25 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; @@ -769,7 +772,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 <> diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 3a1ce8949d1..04a80e34759 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -160,6 +160,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 3e9a9a879cc..605743c18f0 100644 --- a/tests/extra_python_package/test_files.py +++ b/tests/extra_python_package/test_files.py @@ -36,6 +36,7 @@ "include/pybind11/functional.h", "include/pybind11/gil.h", "include/pybind11/iostream.h", + "include/pybind11/native_enum.h", "include/pybind11/numpy.h", "include/pybind11/operators.h", "include/pybind11/options.h", @@ -56,6 +57,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/smart_holder_poc.h", "include/pybind11/detail/smart_holder_sfinae_hooks_only.h", "include/pybind11/detail/smart_holder_type_casters.h", diff --git a/tests/test_enum.cpp b/tests/test_enum.cpp index 2597b275ef6..4ec0af7245b 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 4e85d29c31f..997ac6c5e01 100644 --- a/tests/test_enum.py +++ b/tests/test_enum.py @@ -264,3 +264,12 @@ def test_docstring_signatures(): for attr in enum_type.__dict__.values(): # Issue #2623/PR #2637: Add argument names to enum_ methods assert "arg0" not in (attr.__doc__ or "") + + +@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 00000000000..b0c1aae88c2 --- /dev/null +++ b/tests/test_native_enum.cpp @@ -0,0 +1,185 @@ +#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.attr("native_enum_type_map_abi_id_c_str") + = py::cross_extension_shared_states::native_enum_type_map::abi_id(); + + m += py::native_enum("smallenum") + .value("a", smallenum::a) + .value("b", smallenum::b) + .value("c", smallenum::c); + + m += py::native_enum("color") + .value("red", color::red) + .value("yellow", color::yellow) + .value("green", color::green) + .value("blue", color::blue); + + m += py::native_enum("altitude") + .value("high", altitude::high) + .value("low", altitude::low); + + m += py::native_enum("export_values") + .value("exv0", export_values::exv0) + .value("exv1", export_values::exv1) + .export_values(); + + m += py::native_enum("member_doc") + .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") + .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}; + }); + + m.def("native_enum_value_malformed_utf8", [](const char *malformed_utf8) { + enum fake { x }; + py::native_enum("fake").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").value("x", fake::x); + py::native_enum("fake_double_registration_native_enum"); + }); + + m.def("native_enum_name_clash", [](py::module_ m) { + enum fake { x }; + m += py::native_enum("fake_native_enum_name_clash").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") + .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").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").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").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 00000000000..53d36e33a69 --- /dev/null +++ b/tests/test_native_enum.py @@ -0,0 +1,208 @@ +import enum +import re + +import pytest + +from pybind11_tests import native_enum as m + + +def test_abi_id(): + assert re.match( + "__pybind11_native_enum_type_map_v1_.*__$", m.native_enum_type_map_abi_id_c_str + ) + + +SMALLENUM_MEMBERS = ( + ("a", 0), + ("b", 1), + ("c", 2), +) + +COLOR_MEMBERS = ( + ("red", 0), + ("yellow", 1), + ("green", 20), + ("blue", 21), +) + +ALTITUDE_MEMBERS = ( + ("high", "h"), + ("low", "l"), +) + +EXPORT_VALUES_MEMBERS = ( + ("exv0", 0), + ("exv1", 1), +) + +MEMBER_DOC_MEMBERS = ( + ("mem0", 0), + ("mem1", 1), + ("mem2", 2), +) + + +@pytest.mark.parametrize( + "enum_type", [m.smallenum, m.color, m.altitude, m.export_values, m.member_doc] +) +def test_enum_type(enum_type): + assert isinstance(enum_type, enum.EnumMeta) + + +@pytest.mark.parametrize( + ("enum_type", "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), + ], +) +def test_enum_members(enum_type, members): + for name, value in members: + assert enum_type[name].value == value + + +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_class_with_enum(): + for value, name in enumerate(("one", "two")): + assert m.class_with_enum.in_class[name].value == value + + +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.")