Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[smart_holder] Conversions between Python's native (stdlib) enum types and C++ enums. #5280

Draft
wants to merge 13 commits into
base: smart_holder
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ set(PYBIND11_HEADERS
include/pybind11/detail/dynamic_raw_ptr_cast_if_possible.h
include/pybind11/detail/init.h
include/pybind11/detail/internals.h
include/pybind11/detail/native_enum_data.h
include/pybind11/detail/struct_smart_holder.h
include/pybind11/detail/type_caster_base.h
include/pybind11/detail/typeid.h
Expand All @@ -155,6 +156,7 @@ set(PYBIND11_HEADERS
include/pybind11/gil_safe_call_once.h
include/pybind11/iostream.h
include/pybind11/functional.h
include/pybind11/native_enum.h
include/pybind11/numpy.h
include/pybind11/operators.h
include/pybind11/pybind11.h
Expand Down
5 changes: 5 additions & 0 deletions docs/classes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -553,3 +553,8 @@ The ``name`` property returns the name of the enum value as a unicode string.
.. warning::

Contrary to Python customs, enum values from the wrappers should not be compared using ``is``, but with ``==`` (see `#1177 <https://github.com/pybind/pybind11/issues/1177>`_ for background).

.. note::

``py::native_enum`` was added as an alternative to ``py::enum_``
with http://github.com/pybind/pybind11/pull/5280
110 changes: 109 additions & 1 deletion include/pybind11/cast.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

#include "detail/common.h"
#include "detail/descr.h"
#include "detail/native_enum_data.h"
#include "detail/type_caster_base.h"
#include "detail/typeid.h"
#include "pytypes.h"
Expand Down Expand Up @@ -53,6 +54,104 @@ cast_op(make_caster<T> &&caster) {
return std::move(caster).operator result_t();
}

template <typename EnumType>
class type_caster_enum_type {
private:
using Underlying = typename std::underlying_type<EnumType>::type;

public:
static constexpr auto name = const_name<EnumType>();

template <typename SrcType>
static handle cast(SrcType &&src, return_value_policy, handle parent) {
handle native_enum
= global_internals_native_enum_type_map_get_item(std::type_index(typeid(EnumType)));
if (native_enum) {
return native_enum(static_cast<Underlying>(src)).release();
}
return type_caster_base<EnumType>::cast(
std::forward<SrcType>(src),
// Fixes https://github.com/pybind/pybind11/pull/3643#issuecomment-1022987818:
return_value_policy::copy,
parent);
}

bool load(handle src, bool convert) {
handle native_enum
= global_internals_native_enum_type_map_get_item(std::type_index(typeid(EnumType)));
if (native_enum) {
if (!isinstance(src, native_enum)) {
return false;
}
type_caster<Underlying> underlying_caster;
if (!underlying_caster.load(src.attr("value"), convert)) {
pybind11_fail("native_enum internal consistency failure.");
}
value = static_cast<EnumType>(static_cast<Underlying>(underlying_caster));
return true;
}
if (!pybind11_enum_) {
pybind11_enum_.reset(new type_caster_base<EnumType>());
}
return pybind11_enum_->load(src, convert);
}

template <typename T>
using cast_op_type = detail::cast_op_type<T>;

// NOLINTNEXTLINE(google-explicit-constructor)
operator EnumType *() {
if (!pybind11_enum_) {
return &value;
}
return pybind11_enum_->operator EnumType *();
}

// NOLINTNEXTLINE(google-explicit-constructor)
operator EnumType &() {
if (!pybind11_enum_) {
return value;
}
return pybind11_enum_->operator EnumType &();
}

private:
std::unique_ptr<type_caster_base<EnumType>> pybind11_enum_;
EnumType value;
};

template <typename EnumType, typename SFINAE = void>
struct type_caster_enum_type_enabled : std::true_type {};

template <typename T>
struct type_uses_type_caster_enum_type {
static constexpr bool value
= std::is_enum<T>::value && type_caster_enum_type_enabled<T>::value;
};

template <typename EnumType>
class type_caster<EnumType, detail::enable_if_t<type_uses_type_caster_enum_type<EnumType>::value>>
: public type_caster_enum_type<EnumType> {};

template <typename T, detail::enable_if_t<std::is_enum<T>::value, int> = 0>
bool isinstance_native_enum_impl(handle obj, const std::type_info &tp) {
handle native_enum = global_internals_native_enum_type_map_get_item(tp);
if (!native_enum) {
return false;
}
return isinstance(obj, native_enum);
}

template <typename T, detail::enable_if_t<!std::is_enum<T>::value, int> = 0>
bool isinstance_native_enum_impl(handle, const std::type_info &) {
return false;
}

template <typename T>
bool isinstance_native_enum(handle obj, const std::type_info &tp) {
return isinstance_native_enum_impl<intrinsic_t<T>>(obj, tp);
}

template <typename type>
class type_caster<std::reference_wrapper<type>> {
private:
Expand Down Expand Up @@ -1462,8 +1561,17 @@ template <typename T,
= 0>
T cast(const handle &handle) {
using namespace detail;
static_assert(!cast_is_temporary_value_reference<T>::value,
constexpr bool is_enum_cast = type_uses_type_caster_enum_type<intrinsic_t<T>>::value;
static_assert(!cast_is_temporary_value_reference<T>::value || is_enum_cast,
"Unable to cast type to reference: value is local to type caster");
#ifndef NDEBUG
if (is_enum_cast && cast_is_temporary_value_reference<T>::value) {
if (detail::global_internals_native_enum_type_map_contains(
std::type_index(typeid(intrinsic_t<T>)))) {
pybind11_fail("Unable to cast native enum type to reference");
}
}
#endif
return cast_op<T>(load_type<T>(handle));
}

Expand Down
10 changes: 1 addition & 9 deletions include/pybind11/detail/class.h
Original file line number Diff line number Diff line change
Expand Up @@ -636,15 +636,7 @@ inline PyObject *make_new_python_type(const type_record &rec) {
PyUnicode_FromFormat("%U.%U", rec.scope.attr("__qualname__").ptr(), name.ptr()));
}

object module_;
if (rec.scope) {
if (hasattr(rec.scope, "__module__")) {
module_ = rec.scope.attr("__module__");
} else if (hasattr(rec.scope, "__name__")) {
module_ = rec.scope.attr("__name__");
}
}

object module_ = get_module_name_if_available(rec.scope);
const auto *full_name = c_str(
#if !defined(PYPY_VERSION)
module_ ? str(module_).cast<std::string>() + "." + rec.name :
Expand Down
4 changes: 4 additions & 0 deletions include/pybind11/detail/internals.h
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,10 @@ struct internals {
std::string function_record_capsule_name = internals_function_record_capsule_name;
#endif

#if PYBIND11_INTERNALS_VERSION >= 6
type_map<PyObject *> native_enum_type_map;
#endif

internals() = default;
internals(const internals &other) = delete;
internals &operator=(const internals &other) = delete;
Expand Down
123 changes: 123 additions & 0 deletions include/pybind11/detail/native_enum_data.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// Copyright (c) 2022 The pybind Community.
// All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

#pragma once

#define PYBIND11_HAS_NATIVE_ENUM

#include "../pytypes.h"
#include "common.h"
#include "internals.h"

#include <string>
#include <typeindex>

PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE)
PYBIND11_NAMESPACE_BEGIN(detail)

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

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

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

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

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

private:
mutable bool correct_use_check{false};

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

inline void global_internals_native_enum_type_map_set_item(const std::type_index &enum_type_index,
PyObject *py_enum) {
with_internals(
[&](internals &internals) { internals.native_enum_type_map[enum_type_index] = py_enum; });
}

inline handle
global_internals_native_enum_type_map_get_item(const std::type_index &enum_type_index) {
return with_internals([&](internals &internals) {
auto found = internals.native_enum_type_map.find(enum_type_index);
if (found != internals.native_enum_type_map.end()) {
return handle(found->second);
}
return handle();
});
}

inline bool
global_internals_native_enum_type_map_contains(const std::type_index &enum_type_index) {
return with_internals([&](internals &internals) {
return internals.native_enum_type_map.count(enum_type_index) != 0;
});
}

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

PYBIND11_NAMESPACE_END(detail)
PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE)
63 changes: 63 additions & 0 deletions include/pybind11/native_enum.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Copyright (c) 2022 The pybind Community.
// All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

#pragma once

#include "detail/common.h"
#include "detail/native_enum_data.h"
#include "detail/type_caster_base.h"
#include "cast.h"

#include <limits>
#include <type_traits>
#include <typeindex>

PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE)

enum class native_enum_kind { Enum, IntEnum };

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

explicit native_enum(const char *name, native_enum_kind kind)
: detail::native_enum_data(
name, std::type_index(typeid(Type)), kind == native_enum_kind::IntEnum) {
if (detail::get_local_type_info(typeid(Type)) != nullptr
|| detail::get_global_type_info(typeid(Type)) != nullptr) {
pybind11_fail(
"pybind11::native_enum<...>(\"" + enum_name_encoded
+ "\") is already registered as a `pybind11::enum_` or `pybind11::class_`!");
}
if (detail::global_internals_native_enum_type_map_contains(enum_type_index)) {
pybind11_fail("pybind11::native_enum<...>(\"" + enum_name_encoded
+ "\") is already registered!");
}
arm_correct_use_check();
}

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

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

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

PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE)
Loading