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 all 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
131 changes: 131 additions & 0 deletions .github/workflows/python312.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
name: Python312

on:
workflow_dispatch:
pull_request:

concurrency:
group: python312-${{ github.ref }}
cancel-in-progress: false

env:
PYTEST_TIMEOUT: 300

jobs:
standard:
name: "🐍 3.12 latest • ubuntu-latest • x64"
runs-on: ubuntu-latest
# if: "contains(github.event.pull_request.labels.*.name, 'python dev')"

steps:
- name: Show env
run: env

- uses: actions/checkout@v3

- name: Setup Python 3.12
uses: actions/setup-python@v4
with:
python-version: "3.12-dev"

- name: Setup Boost
run: sudo apt-get install libboost-dev

- name: Update CMake
uses: jwlawson/[email protected]

- name: Run pip installs
run: |
python -m pip install --upgrade pip
python -m pip install --prefer-binary -r tests/requirements.txt
python -m pip install --prefer-binary numpy
# python -m pip install --prefer-binary scipy

- name: Show platform info
run: python -m platform

- name: Show CMake version
run: cmake --version

# FIRST BUILD
- name: Configure C++11
run: >
cmake -S . -B build11
-DCMAKE_VERBOSE_MAKEFILE=ON
-DPYBIND11_WERROR=ON
-DDOWNLOAD_CATCH=ON
-DDOWNLOAD_EIGEN=ON
-DCMAKE_CXX_STANDARD=11
-DCMAKE_BUILD_TYPE=Debug

- name: Build C++11
run: cmake --build build11 -j 2

- name: Python tests C++11
run: cmake --build build11 --target pytest -j 2

- name: C++ tests C++11
run: cmake --build build11 --target cpptest -j 2

- name: Interface test C++11
run: cmake --build build11 --target test_cmake_build

- name: Clean directory
run: git clean -fdx

# SECOND BUILD
- name: Configure C++17
run: >
cmake -S . -B build17
-DCMAKE_VERBOSE_MAKEFILE=ON
-DPYBIND11_WERROR=ON
-DDOWNLOAD_CATCH=ON
-DDOWNLOAD_EIGEN=ON
-DCMAKE_CXX_STANDARD=17
-DCMAKE_BUILD_TYPE=Debug

- name: Build C++17
run: cmake --build build17 -j 2

- name: Python tests C++17
run: cmake --build build17 --target pytest

- name: C++ tests C++17
run: cmake --build build17 --target cpptest

- name: Interface test C++17
run: cmake --build build17 --target test_cmake_build

- name: Clean directory
run: git clean -fdx

# THIRD BUILD
- name: Configure C++17 max DPYBIND11_INTERNALS_VERSION
run: >
cmake -S . -B build17max
-DCMAKE_VERBOSE_MAKEFILE=ON
-DPYBIND11_WERROR=ON
-DDOWNLOAD_CATCH=ON
-DDOWNLOAD_EIGEN=ON
-DCMAKE_CXX_STANDARD=17
-DCMAKE_BUILD_TYPE=Debug
-DPYBIND11_INTERNALS_VERSION=10000000

- name: Build C++17 max DPYBIND11_INTERNALS_VERSION
run: cmake --build build17max -j 2

- name: Python tests C++17 max DPYBIND11_INTERNALS_VERSION
run: cmake --build build17max --target pytest

- name: C++ tests C++17 max DPYBIND11_INTERNALS_VERSION
run: cmake --build build17max --target cpptest

- name: Interface test C++17 max DPYBIND11_INTERNALS_VERSION
run: cmake --build build17max --target test_cmake_build

# Ensure the setup_helpers module can build packages using setuptools
- name: Setuptools helpers test
run: pytest tests/extra_setuptools

- name: Clean directory
run: git clean -fdx
5 changes: 5 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -111,12 +111,16 @@ cmake_dependent_option(PYBIND11_FINDPYTHON "Force new FindPython" OFF

# NB: when adding a header don't forget to also add it to setup.py
set(PYBIND11_HEADERS
include/pybind11/detail/abi_platform_id.h
include/pybind11/detail/class.h
include/pybind11/detail/common.h
include/pybind11/detail/cross_extension_shared_state.h
include/pybind11/detail/descr.h
include/pybind11/detail/init.h
include/pybind11/detail/internals.h
include/pybind11/detail/native_enum_data.h
include/pybind11/detail/type_caster_base.h
include/pybind11/detail/type_map.h
include/pybind11/detail/typeid.h
include/pybind11/attr.h
include/pybind11/buffer_info.h
Expand All @@ -133,6 +137,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
Expand Down
109 changes: 108 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 @@ -48,6 +49,101 @@ cast_op(make_caster<T> &&caster) {
template cast_op_type<typename std::add_rvalue_reference<T>::type>();
}

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) {
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<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) {
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> 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 EnumType>
class type_caster<EnumType,
detail::enable_if_t<std::is_enum<EnumType>::value
&& type_caster_enum_type_enabled<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) {
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 <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 @@ -1037,8 +1133,19 @@ PYBIND11_NAMESPACE_END(detail)
template <typename T, detail::enable_if_t<!detail::is_pyobject<T>::value, int> = 0>
T cast(const handle &handle) {
using namespace detail;
static_assert(!cast_is_temporary_value_reference<T>::value,
constexpr bool is_enum_cast
= std::is_base_of<type_caster_enum_type<intrinsic_t<T>>, make_caster<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) {
if (cross_extension_shared_states::native_enum_type_map::get().count(
std::type_index(typeid(intrinsic_t<T>)))
!= 0) {
pybind11_fail("Unable to cast native enum type to reference");
}
}
#endif
return cast_op<T>(load_type<T>(handle));
}

Expand Down
93 changes: 93 additions & 0 deletions include/pybind11/detail/abi_platform_id.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// 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 "common.h"

/// On MSVC, debug and release builds are not ABI-compatible!
#if defined(_MSC_VER) && defined(_DEBUG)
# define PYBIND11_BUILD_TYPE "_debug"
#else
# define PYBIND11_BUILD_TYPE ""
#endif

/// Let's assume that different compilers are ABI-incompatible.
/// A user can manually set this string if they know their
/// compiler is compatible.
#ifndef PYBIND11_COMPILER_TYPE
# if defined(_MSC_VER)
# define PYBIND11_COMPILER_TYPE "_msvc"
# elif defined(__INTEL_COMPILER)
# define PYBIND11_COMPILER_TYPE "_icc"
# elif defined(__clang__)
# define PYBIND11_COMPILER_TYPE "_clang"
# elif defined(__PGI)
# define PYBIND11_COMPILER_TYPE "_pgi"
# elif defined(__MINGW32__)
# define PYBIND11_COMPILER_TYPE "_mingw"
# elif defined(__CYGWIN__)
# define PYBIND11_COMPILER_TYPE "_gcc_cygwin"
# elif defined(__GNUC__)
# define PYBIND11_COMPILER_TYPE "_gcc"
# else
# define PYBIND11_COMPILER_TYPE "_unknown"
# endif
#endif

/// Also standard libs
#ifndef PYBIND11_STDLIB
# if defined(_LIBCPP_VERSION)
# define PYBIND11_STDLIB "_libcpp"
# elif defined(__GLIBCXX__) || defined(__GLIBCPP__)
# define PYBIND11_STDLIB "_libstdcpp"
# else
# define PYBIND11_STDLIB ""
# endif
#endif

/// On Linux/OSX, changes in __GXX_ABI_VERSION__ indicate ABI incompatibility.
#ifndef PYBIND11_BUILD_ABI
# if defined(__GXX_ABI_VERSION)
# define PYBIND11_BUILD_ABI "_cxxabi" PYBIND11_TOSTRING(__GXX_ABI_VERSION)
# else
# define PYBIND11_BUILD_ABI ""
# endif
#endif

#ifndef PYBIND11_INTERNALS_KIND
# if defined(WITH_THREAD)
# define PYBIND11_INTERNALS_KIND ""
# else
# define PYBIND11_INTERNALS_KIND "_without_thread"
# endif
#endif

/* NOTE - ATTENTION - WARNING - EXTREME CAUTION
Changing this will break compatibility with `PYBIND11_INTERNALS_VERSION 4`
See pybind11/detail/type_map.h for more information.
*/
#define PYBIND11_PLATFORM_ABI_ID_V4 \
PYBIND11_INTERNALS_KIND PYBIND11_COMPILER_TYPE PYBIND11_STDLIB PYBIND11_BUILD_ABI \
PYBIND11_BUILD_TYPE

/// LEGACY "ABI-breaking" APPROACH, ORIGINAL COMMENT
/// ------------------------------------------------
/// Tracks the `internals` and `type_info` ABI version independent of the main library version.
///
/// Some portions of the code use an ABI that is conditional depending on this
/// version number. That allows ABI-breaking changes to be "pre-implemented".
/// Once the default version number is incremented, the conditional logic that
/// no longer applies can be removed. Additionally, users that need not
/// maintain ABI compatibility can increase the version number in order to take
/// advantage of any functionality/efficiency improvements that depend on the
/// newer ABI.
///
/// WARNING: If you choose to manually increase the ABI version, note that
/// pybind11 may not be tested as thoroughly with a non-default ABI version, and
/// 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
#endif
Loading