Skip to content

WIP: Conversions between Python's native (stdlib) enum and C++ enums. #4329

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

Closed
wants to merge 35 commits into from
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
f0a398e
First approximation.
rwgk Nov 12, 2022
2da6779
Add pybind11/native_enum.h, building the native enum type. Not used b…
rwgk Nov 12, 2022
1324769
Remove unused code entirely, to not trigger MSVC C4127 with the place…
rwgk Nov 13, 2022
4ab579e
Insert `type_caster<EnumType>`, simply forwarding to `type_caster_bas…
rwgk Nov 13, 2022
4cb7da0
Fit `native_enum` functionality into `type_caster<EnumType>`
rwgk Nov 13, 2022
03ed301
Add `type_caster_enum_type_enabled<ProtoEnumType>` with test.
rwgk Nov 13, 2022
2c9d5a0
Additional tests based on global testing failures: `test_pybind11_isi…
rwgk Nov 14, 2022
7084826
Add `py::native_enum<smallenum>`
rwgk Nov 14, 2022
1a1112c
Shorten expected "Unable to cast" message for compatibility with non-…
rwgk Nov 14, 2022
0a49b5b
Update `cast_is_temporary_value_reference` to exclude `type_caster_en…
rwgk Nov 14, 2022
fdbd560
Move cast ptr test from test_native_enum to test_enum, undo change to…
rwgk Nov 15, 2022
1cec429
Fix oversight: forgot to undo unneeded change.
rwgk Nov 15, 2022
75bb576
Bug fix and test for "Unable to cast native enum type to reference" e…
rwgk Nov 15, 2022
1f3d94d
Add `isinstance_native_enum()` and plug into `isinstance(handle)`
rwgk Nov 15, 2022
b478a55
Make test_native_enum.py output less noisy.
rwgk Nov 15, 2022
ae953a7
Add `enum.Enum` support, for non-integer underlying types.
rwgk Nov 15, 2022
90044fb
Disable `test_obj_cast_unscoped_enum_ptr` & `test_obj_cast_color_ptr`…
rwgk Nov 15, 2022
daa515a
Add `.export_values()` and member `__doc__` implementations with tests.
rwgk Nov 15, 2022
962ebf9
Add missing `<limits>`, while at it, also `<typeindex>` for completen…
rwgk Nov 15, 2022
703372c
Move code for building the native enum type from `native_enum<>` dtor…
rwgk Nov 16, 2022
252524b
Change variable name to avoid MSVC C4458 warning.
rwgk Nov 16, 2022
e0a4eb6
Resolve clang-tidy error (missing `explicit`).
rwgk Nov 16, 2022
f3ece13
Add user-friendly correct-use check (activated in Debug builds only).
rwgk Nov 16, 2022
50fae46
Resolve clang-tidy error (missing `const`).
rwgk Nov 16, 2022
0344e90
Catch & test all double-registration or name clash situations. Fix up…
rwgk Nov 16, 2022
a0f43c9
Resolve clang-tidy error (missing `!= nullptr`).
rwgk Nov 17, 2022
c2e6b38
Modified version of PR #4293 by @wjakob
rwgk Nov 17, 2022
bb6c003
Snapshot of .github/workflows/python312.yml (PR #4342)
rwgk Nov 17, 2022
dd0147a
Update .github/workflows/python312.yml (PR #4342)
rwgk Nov 17, 2022
8737bea
Split out `get_native_enum_type_map()`, leaving `struct internals` un…
rwgk Nov 17, 2022
3debb51
Organize internals-like functionality as `struct native_enum_type_map…
rwgk Nov 18, 2022
61925d2
Make the internals-like mechanism reusable as `cross_extension_shared…
rwgk Nov 18, 2022
3a9ecef
Rearrange code slightly to resolve clang-tidy warning.
rwgk Nov 19, 2022
f29f6e2
Use `detail::native_enum_type_map::get().count()` to simplify code sl…
rwgk Nov 19, 2022
ad1d258
Factor out pybind11/detail/abi_platform_id.h, type_map.h, cross_exten…
rwgk Nov 19, 2022
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
5 changes: 4 additions & 1 deletion include/pybind11/detail/internals.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
/// 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 4
# define PYBIND11_INTERNALS_VERSION 5
#endif

PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE)
Expand Down Expand Up @@ -209,6 +209,9 @@ struct internals {
PYBIND11_TLS_FREE(tstate);
}
#endif
#if PYBIND11_INTERNALS_VERSION > 4
type_map<PyObject *> native_enum_types;
#endif
};

/// Additional type information which does not fit into the PyTypeObject.
Expand Down
67 changes: 67 additions & 0 deletions include/pybind11/native_enum.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// 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 "pybind11.h"

#include <type_traits>

PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE)

/// Conversions between Python's native (stdlib) enum types and C++ enums.
template <typename Type>
class native_enum {
public:
using Underlying = typename std::underlying_type<Type>::type;
// Scalar is the integer representation of underlying type
using Scalar = detail::conditional_t<detail::any_of<detail::is_std_char_type<Underlying>,
std::is_same<Underlying, bool>>::value,
detail::equivalent_integer_t<Underlying>,
Underlying>;

template <typename... Extra>
native_enum(const handle &scope, const char *name, const Extra &.../*extra*/)
: m_scope(reinterpret_borrow<object>(scope)), m_name(name) {
constexpr bool is_arithmetic = detail::any_of<std::is_same<arithmetic, Extra>...>::value;
constexpr bool is_convertible = std::is_convertible<Type, Underlying>::value;
if (is_arithmetic || is_convertible) {
// IGNORED.
}
}

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

/// Add an enumeration entry
native_enum &value(char const *name, Type value, const char *doc = nullptr) {
if (doc) {
// IGNORED.
}
m_members.append(make_tuple(name, static_cast<Scalar>(value)));
return *this;
}

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

~native_enum() {
// Any exception here will terminate the process.
auto enum_module = module_::import("enum");
auto int_enum = enum_module.attr("IntEnum");
auto int_enum_color = int_enum(m_name, m_members);
int_enum_color.attr("__module__") = m_scope;
m_scope.attr(m_name) = int_enum_color;
// Intentionally leak Python reference.
detail::get_internals().native_enum_types[std::type_index(typeid(Type))]
= int_enum_color.release().ptr();
}

private:
object m_scope;
str m_name;
list m_members;
};

PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE)
1 change: 1 addition & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,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
Expand Down
1 change: 1 addition & 0 deletions tests/extra_python_package/test_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
86 changes: 86 additions & 0 deletions tests/test_native_enum.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
#include <pybind11/native_enum.h>

#include "pybind11_tests.h"

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
};

// the constant d is 0, the constant e is 1, the constant f is 3
enum { d, e, f = e + 2 };

int pass_color(color e) { return static_cast<int>(e); }
color return_color(int i) { return static_cast<color>(i); }

py::handle wrap_color(py::module_ m) {
auto enum_module = py::module_::import("enum");
auto int_enum = enum_module.attr("IntEnum");
using u_t = std::underlying_type<color>::type;
auto members = py::make_tuple(py::make_tuple("red", static_cast<u_t>(color::red)),
py::make_tuple("yellow", static_cast<u_t>(color::yellow)),
py::make_tuple("green", static_cast<u_t>(color::green)),
py::make_tuple("blue", static_cast<u_t>(color::blue)));
auto int_enum_color = int_enum("color", members);
int_enum_color.attr("__module__") = m;
m.attr("color") = int_enum_color;
return int_enum_color.release(); // Intentionally leak Python reference.
}

} // namespace test_native_enum

PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE)
PYBIND11_NAMESPACE_BEGIN(detail)

using namespace test_native_enum;

template <>
struct type_caster<color> {
static handle native_type;

static handle cast(const color &src, return_value_policy /* policy */, handle /* parent */) {
auto u_v = static_cast<std::underlying_type<color>::type>(src);
return native_type(u_v).release();
}

bool load(handle src, bool /* convert */) {
if (!isinstance(src, native_type)) {
return false;
}
value = static_cast<color>(py::cast<std::underlying_type<color>::type>(src.attr("value")));
return true;
}

PYBIND11_TYPE_CASTER(color, const_name("<enum 'color'>"));
};

handle type_caster<color>::native_type = nullptr;

PYBIND11_NAMESPACE_END(detail)
PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE)

TEST_SUBMODULE(native_enum, m) {
using namespace test_native_enum;

py::detail::type_caster<color>::native_type = wrap_color(m);

m.def("pass_color", pass_color);
m.def("return_color", return_color);

py::native_enum<color>(m, "WIPcolor")
.value("red", color::red)
.value("yellow", color::yellow)
.value("green", color::green)
.value("blue", color::blue);
}
51 changes: 51 additions & 0 deletions tests/test_native_enum.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import enum

import pytest

from pybind11_tests import native_enum as m

COLOR_MEMBERS = (
("red", 0),
("yellow", 1),
("green", 20),
("blue", 21),
)


def test_enum_color_type():
assert isinstance(m.color, enum.EnumMeta)


@pytest.mark.parametrize("name,value", COLOR_MEMBERS)
def test_enum_color_members(name, value):
assert m.color[name] == value


@pytest.mark.parametrize("name,value", COLOR_MEMBERS)
def test_pass_color_success(name, value):
assert m.pass_color(m.color[name]) == value


def test_pass_color_fail():
with pytest.raises(TypeError) as excinfo:
m.pass_color(None)
assert "<enum 'color'>" in str(excinfo.value)


@pytest.mark.parametrize("name,value", COLOR_MEMBERS)
def test_return_color_success(name, value):
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_wip_color():
assert isinstance(m.WIPcolor, enum.EnumMeta)
for name, value in COLOR_MEMBERS:
assert m.WIPcolor[name] == value