mirror of
https://github.com/pybind/pybind11.git
synced 2026-03-14 20:27:47 +00:00
* 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.
298 lines
8.6 KiB
Python
298 lines
8.6 KiB
Python
from __future__ import annotations
|
|
|
|
import enum
|
|
import pickle
|
|
|
|
import pytest
|
|
|
|
import env
|
|
from pybind11_tests import native_enum as m
|
|
|
|
SMALLENUM_MEMBERS = (
|
|
("a", 0),
|
|
("b", 1),
|
|
("c", 2),
|
|
)
|
|
|
|
COLOR_MEMBERS = (
|
|
("red", 0),
|
|
("yellow", 1),
|
|
("green", 20),
|
|
("blue", 21),
|
|
)
|
|
|
|
ALTITUDE_MEMBERS = (
|
|
("high", "h"),
|
|
("low", "l"),
|
|
)
|
|
|
|
FLAGS_UCHAR_MEMBERS = (
|
|
("bit0", 0x1),
|
|
("bit1", 0x2),
|
|
("bit2", 0x4),
|
|
)
|
|
|
|
FLAGS_UINT_MEMBERS = (
|
|
("bit0", 0x1),
|
|
("bit1", 0x2),
|
|
("bit2", 0x4),
|
|
)
|
|
|
|
CLASS_WITH_ENUM_IN_CLASS_MEMBERS = (
|
|
("one", 0),
|
|
("two", 1),
|
|
)
|
|
|
|
EXPORT_VALUES_MEMBERS = (
|
|
("exv0", 0),
|
|
("exv1", 1),
|
|
)
|
|
|
|
MEMBER_DOC_MEMBERS = (
|
|
("mem0", 0),
|
|
("mem1", 1),
|
|
("mem2", 2),
|
|
)
|
|
|
|
ENUM_TYPES_AND_MEMBERS = (
|
|
(m.smallenum, SMALLENUM_MEMBERS),
|
|
(m.color, COLOR_MEMBERS),
|
|
(m.altitude, ALTITUDE_MEMBERS),
|
|
(m.flags_uchar, FLAGS_UCHAR_MEMBERS),
|
|
(m.flags_uint, FLAGS_UINT_MEMBERS),
|
|
(m.export_values, EXPORT_VALUES_MEMBERS),
|
|
(m.member_doc, MEMBER_DOC_MEMBERS),
|
|
(m.class_with_enum.in_class, CLASS_WITH_ENUM_IN_CLASS_MEMBERS),
|
|
)
|
|
|
|
ENUM_TYPES = [_[0] for _ in ENUM_TYPES_AND_MEMBERS]
|
|
|
|
|
|
@pytest.mark.parametrize("enum_type", ENUM_TYPES)
|
|
def test_enum_type(enum_type):
|
|
assert isinstance(enum_type, enum.EnumMeta)
|
|
assert enum_type.__module__ == m.__name__
|
|
|
|
|
|
@pytest.mark.parametrize(("enum_type", "members"), ENUM_TYPES_AND_MEMBERS)
|
|
def test_enum_members(enum_type, members):
|
|
for name, value in members:
|
|
assert enum_type[name].value == value
|
|
|
|
|
|
@pytest.mark.parametrize(("enum_type", "members"), ENUM_TYPES_AND_MEMBERS)
|
|
def test_pickle_roundtrip(enum_type, members):
|
|
for name, _ in members:
|
|
orig = enum_type[name]
|
|
if enum_type is m.class_with_enum.in_class:
|
|
# This is a general pickle limitation.
|
|
with pytest.raises(pickle.PicklingError):
|
|
pickle.dumps(orig)
|
|
else:
|
|
# This only works if __module__ is correct.
|
|
serialized = pickle.dumps(orig)
|
|
restored = pickle.loads(serialized)
|
|
assert restored == orig
|
|
|
|
|
|
@pytest.mark.parametrize("enum_type", [m.flags_uchar, m.flags_uint])
|
|
def test_enum_flag(enum_type):
|
|
bits02 = enum_type.bit0 | enum_type.bit2
|
|
assert enum_type.bit0 in bits02
|
|
assert enum_type.bit1 not in bits02
|
|
assert enum_type.bit2 in bits02
|
|
|
|
|
|
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_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_exercise_import_or_getattr_leading_dot():
|
|
with pytest.raises(ValueError) as excinfo:
|
|
m.exercise_import_or_getattr(m, ".")
|
|
assert str(excinfo.value) == "Invalid fully-qualified name `.` (native_type_name)"
|
|
|
|
|
|
def test_exercise_import_or_getattr_bad_top_level():
|
|
with pytest.raises(ImportError) as excinfo:
|
|
m.exercise_import_or_getattr(m, "NeVeRLaNd")
|
|
assert (
|
|
str(excinfo.value)
|
|
== "Failed to import top-level module `NeVeRLaNd` (native_type_name)"
|
|
)
|
|
|
|
|
|
def test_exercise_import_or_getattr_dot_dot():
|
|
with pytest.raises(ValueError) as excinfo:
|
|
m.exercise_import_or_getattr(m, "enum..")
|
|
assert (
|
|
str(excinfo.value) == "Invalid fully-qualified name `enum..` (native_type_name)"
|
|
)
|
|
|
|
|
|
def test_exercise_import_or_getattr_bad_enum_attr():
|
|
with pytest.raises(ImportError) as excinfo:
|
|
m.exercise_import_or_getattr(m, "enum.NoNeXiStInG")
|
|
lines = str(excinfo.value).splitlines()
|
|
assert len(lines) >= 5
|
|
assert (
|
|
lines[0]
|
|
== "Failed to import or getattr `NoNeXiStInG` from `enum` (native_type_name)"
|
|
)
|
|
assert lines[1] == "-------- getattr exception --------"
|
|
ix = lines.index("-------- import exception --------")
|
|
assert ix >= 3
|
|
assert len(lines) > ix + 0
|
|
|
|
|
|
def test_native_enum_data_missing_finalize_error_message():
|
|
msg = m.native_enum_data_missing_finalize_error_message("Fake")
|
|
assert msg == 'pybind11::native_enum<...>("Fake", ...): MISSING .finalize()'
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"func", [m.native_enum_ctor_malformed_utf8, m.native_enum_value_malformed_utf8]
|
|
)
|
|
def test_native_enum_malformed_utf8(func):
|
|
if env.GRAALPY and func is m.native_enum_ctor_malformed_utf8:
|
|
pytest.skip("GraalPy does not raise UnicodeDecodeError")
|
|
malformed_utf8 = b"\x80"
|
|
with pytest.raises(UnicodeDecodeError):
|
|
func(malformed_utf8)
|
|
|
|
|
|
def test_native_enum_double_finalize():
|
|
with pytest.raises(RuntimeError) as excinfo:
|
|
m.native_enum_double_finalize(m)
|
|
assert (
|
|
str(excinfo.value)
|
|
== 'pybind11::native_enum<...>("fake_native_enum_double_finalize"): DOUBLE finalize'
|
|
)
|
|
|
|
|
|
def test_native_enum_value_after_finalize():
|
|
with pytest.raises(RuntimeError) as excinfo:
|
|
m.native_enum_value_after_finalize(m)
|
|
assert (
|
|
str(excinfo.value)
|
|
== 'pybind11::native_enum<...>("fake_native_enum_value_after_finalize"): value after finalize'
|
|
)
|
|
|
|
|
|
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_missing_finalize_failure():
|
|
if not isinstance(m.native_enum_missing_finalize_failure, str):
|
|
m.native_enum_missing_finalize_failure()
|
|
pytest.fail("Process termination expected.")
|