Files
pybind11/tests/test_enum.cpp
Ralf W. Grosse-Kunstleve f365314ec0 Enable Conversions Between Native Python Enum Types and C++ Enums (#5555)
* Apply smart_holder-branch-based PR #5280 on top of master.

* Add pytest.skip("GraalPy does not raise UnicodeDecodeError")

* Add `parent_scope` as first argument to `py::native_enum` ctor.

* Replace `operator+=` API with `.finalize()` API. The error messages still need cleanup.

* Resolve clang-tidy performance-unnecessary-value-param errors

* Rename (effectively) native_enum_add_to_parent() -> finalize()

* Update error message: pybind11::native_enum<...>("Fake", ...): MISSING .finalize()

* Pass py::module_ by reference to resolve clang-tidy errors (this is entirely inconsequential otherwise for all practical purposes).

* test_native_enum_correct_use_failure -> test_native_enum_missing_finalize_failure

* Add test_native_enum_double_finalize(), test_native_enum_value_after_finalize()

* Clean up public/protected API.

* [ci skip] Update the Enumerations section in classes.rst

* Rename `py::native_enum_kind` → `py::enum_kind` as suggested by gh-henryiii:

https://github.com/pybind/pybind11/pull/5555#issuecomment-2711672335

* 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        = <enum 'StrEnum'>
        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)
        _          = <class 'str'>
        bases      = (<enum 'StrEnum'>,)
        boundary   = None
        class_name = 'greek'
        classdict  = {'_generate_next_value_': <function StrEnum._generate_next_value_ at 0x701ec1711e40>, 'Alpha': 10, 'Omega': 20, '__module__': 'test_native_enum'}
        cls        = <enum 'StrEnum'>
        first_enum = <enum 'StrEnum'>
        item       = ('Omega', 20)
        member_name = 'Omega'
        member_value = 20
        metacls    = <class 'enum.EnumType'>
        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__  = <class 'enum.EnumType'>
        __new__    = <function StrEnum.__new__ at 0x701ec1711da0>
        _gnv       = <staticmethod(<function StrEnum._generate_next_value_ at 0x701ec1711e40>)>
        _order_    = None
        _simple    = False
        bases      = (<enum 'StrEnum'>,)
        boundary   = None
        classdict  = {'Alpha': <enum._proto_member object at 0x701ebc74f9b0>, 'Omega': <enum._proto_member object at 0x701ebc74cce0>, '__module__': 'test_native_enum', '_all_bits_': 0, ...}
        cls        = 'greek'
        exc        = TypeError('10 is not a string')
        first_enum = <enum 'StrEnum'>
        ignore     = ['_ignore_']
        invalid_names = set()
        key        = '_ignore_'
        kwds       = {}
        member_names = {'Alpha': None, 'Omega': None}
        member_type = <class 'str'>
        metacls    = <class 'enum.EnumType'>
        name       = 'Omega'
        save_new   = False
        tb         = <traceback object at 0x701ebc7a6cc0>
        use_args   = True
        value      = 20
/usr/lib/python3.12/enum.py:596: in __new__
    enum_class = super().__new__(metacls, cls, bases, classdict, **kwds)
        __class__  = <class 'enum.EnumType'>
        __new__    = <function StrEnum.__new__ at 0x701ec1711da0>
        _gnv       = <staticmethod(<function StrEnum._generate_next_value_ at 0x701ec1711e40>)>
        _order_    = None
        _simple    = False
        bases      = (<enum 'StrEnum'>,)
        boundary   = None
        classdict  = {'Alpha': <enum._proto_member object at 0x701ebc74f9b0>, 'Omega': <enum._proto_member object at 0x701ebc74cce0>, '__module__': 'test_native_enum', '_all_bits_': 0, ...}
        cls        = 'greek'
        exc        = TypeError('10 is not a string')
        first_enum = <enum 'StrEnum'>
        ignore     = ['_ignore_']
        invalid_names = set()
        key        = '_ignore_'
        kwds       = {}
        member_names = {'Alpha': None, 'Omega': None}
        member_type = <class 'str'>
        metacls    = <class 'enum.EnumType'>
        name       = 'Omega'
        save_new   = False
        tb         = <traceback object at 0x701ebc7a6cc0>
        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 = <enum 'greek'>
        member_name = 'Alpha'
        self       = <enum._proto_member object at 0x701ebc74f9b0>
        value      = 10
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

cls = <enum 'greek'>, 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        = <enum 'greek'>
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
```

* Remove StrEnum code.

* Make enum_kind::Enum the default kind.

* Catch redundant .export_values() calls.

* [ci skip] Add back original documentation for `py::enum_` under new advanced/deprecated.rst

* [ci skip] Add documentation for `py::enum_kind` and `py::detail::type_caster_enum_type_enabled`

* Rename `Type` to `EnumType` for readability.

* Eliminate py::enum_kind, use "enum.Enum", "enum.IntEnum" directly. This is still WIP.

* EXPERIMENTAL StrEnum code. To be removed.

* Remove experimental StrEnum code:

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.

* Add test with enum.IntFlag (no production code changes required).

* First import_or_getattr() implementation (dedicated tests are still missing).

* Fix import_or_getattr() implementation, add tests, fix clang-tidy errors.

* [ci skip] Update classes.rst: replace `py::enum_kind` with `native_type_name`

* 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).

* 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.
2025-03-24 20:31:59 -07:00

150 lines
6.2 KiB
C++

/*
tests/test_enums.cpp -- enumerations
Copyright (c) 2016 Wenzel Jakob <wenzel.jakob@epfl.ch>
All rights reserved. Use of this source code is governed by a
BSD-style license that can be found in the LICENSE file.
*/
#include "pybind11_tests.h"
TEST_SUBMODULE(enums, m) {
// test_unscoped_enum
enum UnscopedEnum { EOne = 1, ETwo, EThree };
py::enum_<UnscopedEnum>(m, "UnscopedEnum", py::arithmetic(), "An unscoped enumeration")
.value("EOne", EOne, "Docstring for EOne")
.value("ETwo", ETwo, "Docstring for ETwo")
.value("EThree", EThree, "Docstring for EThree")
.export_values();
// test_scoped_enum
enum class ScopedEnum { Two = 2, Three };
py::enum_<ScopedEnum>(m, "ScopedEnum", py::arithmetic())
.value("Two", ScopedEnum::Two)
.value("Three", ScopedEnum::Three);
m.def("test_scoped_enum", [](ScopedEnum z) {
return "ScopedEnum::" + std::string(z == ScopedEnum::Two ? "Two" : "Three");
});
// test_binary_operators
enum Flags { Read = 4, Write = 2, Execute = 1 };
py::enum_<Flags>(m, "Flags", py::arithmetic())
.value("Read", Flags::Read)
.value("Write", Flags::Write)
.value("Execute", Flags::Execute)
.export_values();
// test_implicit_conversion
class ClassWithUnscopedEnum {
public:
enum EMode { EFirstMode = 1, ESecondMode };
static EMode test_function(EMode mode) { return mode; }
};
py::class_<ClassWithUnscopedEnum> exenum_class(m, "ClassWithUnscopedEnum");
exenum_class.def_static("test_function", &ClassWithUnscopedEnum::test_function);
py::enum_<ClassWithUnscopedEnum::EMode>(exenum_class, "EMode")
.value("EFirstMode", ClassWithUnscopedEnum::EFirstMode)
.value("ESecondMode", ClassWithUnscopedEnum::ESecondMode)
.export_values();
// test_enum_to_int
m.def("test_enum_to_int", [](int) {});
m.def("test_enum_to_uint", [](uint32_t) {});
m.def("test_enum_to_long_long", [](long long) {});
// test_duplicate_enum_name
enum SimpleEnum { ONE, TWO, THREE };
m.def("register_bad_enum", [m]() {
py::enum_<SimpleEnum>(m, "SimpleEnum")
.value("ONE", SimpleEnum::ONE) // NOTE: all value function calls are called with the
// same first parameter value
.value("ONE", SimpleEnum::TWO)
.value("ONE", SimpleEnum::THREE)
.export_values();
});
// test_enum_scalar
enum UnscopedUCharEnum : unsigned char {};
enum class ScopedShortEnum : short {};
enum class ScopedLongEnum : long {};
enum UnscopedUInt64Enum : std::uint64_t {};
static_assert(
py::detail::all_of<
std::is_same<py::enum_<UnscopedUCharEnum>::Scalar, unsigned char>,
std::is_same<py::enum_<ScopedShortEnum>::Scalar, short>,
std::is_same<py::enum_<ScopedLongEnum>::Scalar, long>,
std::is_same<py::enum_<UnscopedUInt64Enum>::Scalar, std::uint64_t>>::value,
"Error during the deduction of enum's scalar type with normal integer underlying");
// test_enum_scalar_with_char_underlying
enum class ScopedCharEnum : char { Zero, Positive };
enum class ScopedWCharEnum : wchar_t { Zero, Positive };
enum class ScopedChar32Enum : char32_t { Zero, Positive };
enum class ScopedChar16Enum : char16_t { Zero, Positive };
// test the scalar of char type enums according to chapter 'Character types'
// from https://en.cppreference.com/w/cpp/language/types
static_assert(
py::detail::any_of<
std::is_same<py::enum_<ScopedCharEnum>::Scalar, signed char>, // e.g. gcc on x86
std::is_same<py::enum_<ScopedCharEnum>::Scalar, unsigned char> // e.g. arm linux
>::value,
"char should be cast to either signed char or unsigned char");
static_assert(sizeof(py::enum_<ScopedWCharEnum>::Scalar) == 2
|| sizeof(py::enum_<ScopedWCharEnum>::Scalar) == 4,
"wchar_t should be either 16 bits (Windows) or 32 (everywhere else)");
static_assert(
py::detail::all_of<
std::is_same<py::enum_<ScopedChar32Enum>::Scalar, std::uint_least32_t>,
std::is_same<py::enum_<ScopedChar16Enum>::Scalar, std::uint_least16_t>>::value,
"char32_t, char16_t (and char8_t)'s size, signedness, and alignment is determined");
#if defined(PYBIND11_HAS_U8STRING)
enum class ScopedChar8Enum : char8_t { Zero, Positive };
static_assert(std::is_same<py::enum_<ScopedChar8Enum>::Scalar, unsigned char>::value);
#endif
// test_char_underlying_enum
py::enum_<ScopedCharEnum>(m, "ScopedCharEnum")
.value("Zero", ScopedCharEnum::Zero)
.value("Positive", ScopedCharEnum::Positive);
py::enum_<ScopedWCharEnum>(m, "ScopedWCharEnum")
.value("Zero", ScopedWCharEnum::Zero)
.value("Positive", ScopedWCharEnum::Positive);
py::enum_<ScopedChar32Enum>(m, "ScopedChar32Enum")
.value("Zero", ScopedChar32Enum::Zero)
.value("Positive", ScopedChar32Enum::Positive);
py::enum_<ScopedChar16Enum>(m, "ScopedChar16Enum")
.value("Zero", ScopedChar16Enum::Zero)
.value("Positive", ScopedChar16Enum::Positive);
// test_bool_underlying_enum
enum class ScopedBoolEnum : bool { FALSE, TRUE };
// bool is unsigned (std::is_signed returns false) and 1-byte long, so represented with u8
static_assert(std::is_same<py::enum_<ScopedBoolEnum>::Scalar, std::uint8_t>::value, "");
py::enum_<ScopedBoolEnum>(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<UnscopedEnum>(obj)) {
if (*obj.cast<UnscopedEnum *>() == UnscopedEnum::ETwo) {
return 2;
}
return 1;
}
return 0;
});
#endif
}