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.
343 lines
11 KiB
Python
343 lines
11 KiB
Python
# ruff: noqa: SIM201 SIM300 SIM202
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
|
|
import pytest
|
|
|
|
import env # noqa: F401
|
|
from pybind11_tests import enums as m
|
|
|
|
|
|
@pytest.mark.xfail("env.GRAALPY", reason="TODO should get fixed on GraalPy side")
|
|
def test_unscoped_enum():
|
|
assert str(m.UnscopedEnum.EOne) == "UnscopedEnum.EOne"
|
|
assert str(m.UnscopedEnum.ETwo) == "UnscopedEnum.ETwo"
|
|
assert str(m.EOne) == "UnscopedEnum.EOne"
|
|
assert repr(m.UnscopedEnum.EOne) == "<UnscopedEnum.EOne: 1>"
|
|
assert repr(m.UnscopedEnum.ETwo) == "<UnscopedEnum.ETwo: 2>"
|
|
assert repr(m.EOne) == "<UnscopedEnum.EOne: 1>"
|
|
|
|
# name property
|
|
assert m.UnscopedEnum.EOne.name == "EOne"
|
|
assert m.UnscopedEnum.EOne.value == 1
|
|
assert m.UnscopedEnum.ETwo.name == "ETwo"
|
|
assert m.UnscopedEnum.ETwo.value == 2
|
|
assert m.EOne is m.UnscopedEnum.EOne
|
|
# name, value readonly
|
|
with pytest.raises(AttributeError):
|
|
m.UnscopedEnum.EOne.name = ""
|
|
with pytest.raises(AttributeError):
|
|
m.UnscopedEnum.EOne.value = 10
|
|
# name, value returns a copy
|
|
# TODO: Neither the name nor value tests actually check against aliasing.
|
|
# Use a mutable type that has reference semantics.
|
|
nonaliased_name = m.UnscopedEnum.EOne.name
|
|
nonaliased_name = "bar" # noqa: F841
|
|
assert m.UnscopedEnum.EOne.name == "EOne"
|
|
nonaliased_value = m.UnscopedEnum.EOne.value
|
|
nonaliased_value = 10 # noqa: F841
|
|
assert m.UnscopedEnum.EOne.value == 1
|
|
|
|
# __members__ property
|
|
assert m.UnscopedEnum.__members__ == {
|
|
"EOne": m.UnscopedEnum.EOne,
|
|
"ETwo": m.UnscopedEnum.ETwo,
|
|
"EThree": m.UnscopedEnum.EThree,
|
|
}
|
|
# __members__ readonly
|
|
with pytest.raises(AttributeError):
|
|
m.UnscopedEnum.__members__ = {}
|
|
# __members__ returns a copy
|
|
nonaliased_members = m.UnscopedEnum.__members__
|
|
nonaliased_members["bar"] = "baz"
|
|
assert m.UnscopedEnum.__members__ == {
|
|
"EOne": m.UnscopedEnum.EOne,
|
|
"ETwo": m.UnscopedEnum.ETwo,
|
|
"EThree": m.UnscopedEnum.EThree,
|
|
}
|
|
|
|
for docstring_line in """An unscoped enumeration
|
|
|
|
Members:
|
|
|
|
EOne : Docstring for EOne
|
|
|
|
ETwo : Docstring for ETwo
|
|
|
|
EThree : Docstring for EThree""".split("\n"):
|
|
assert docstring_line in m.UnscopedEnum.__doc__
|
|
|
|
# Unscoped enums will accept ==/!= int comparisons
|
|
y = m.UnscopedEnum.ETwo
|
|
assert y == 2
|
|
assert 2 == y
|
|
assert y != 3
|
|
assert 3 != y
|
|
# Compare with None
|
|
assert y != None # noqa: E711
|
|
assert not (y == None) # noqa: E711
|
|
# Compare with an object
|
|
assert y != object()
|
|
assert not (y == object())
|
|
# Compare with string
|
|
assert y != "2"
|
|
assert "2" != y
|
|
assert not ("2" == y)
|
|
assert not (y == "2")
|
|
|
|
with pytest.raises(TypeError):
|
|
y < object() # noqa: B015
|
|
|
|
with pytest.raises(TypeError):
|
|
y <= object() # noqa: B015
|
|
|
|
with pytest.raises(TypeError):
|
|
y > object() # noqa: B015
|
|
|
|
with pytest.raises(TypeError):
|
|
y >= object() # noqa: B015
|
|
|
|
with pytest.raises(TypeError):
|
|
y | object()
|
|
|
|
with pytest.raises(TypeError):
|
|
y & object()
|
|
|
|
with pytest.raises(TypeError):
|
|
y ^ object()
|
|
|
|
assert int(m.UnscopedEnum.ETwo) == 2
|
|
assert str(m.UnscopedEnum(2)) == "UnscopedEnum.ETwo"
|
|
|
|
# order
|
|
assert m.UnscopedEnum.EOne < m.UnscopedEnum.ETwo
|
|
assert m.UnscopedEnum.EOne < 2
|
|
assert m.UnscopedEnum.ETwo > m.UnscopedEnum.EOne
|
|
assert m.UnscopedEnum.ETwo > 1
|
|
assert m.UnscopedEnum.ETwo <= 2
|
|
assert m.UnscopedEnum.ETwo >= 2
|
|
assert m.UnscopedEnum.EOne <= m.UnscopedEnum.ETwo
|
|
assert m.UnscopedEnum.EOne <= 2
|
|
assert m.UnscopedEnum.ETwo >= m.UnscopedEnum.EOne
|
|
assert m.UnscopedEnum.ETwo >= 1
|
|
assert not (m.UnscopedEnum.ETwo < m.UnscopedEnum.EOne)
|
|
assert not (2 < m.UnscopedEnum.EOne)
|
|
|
|
# arithmetic
|
|
assert m.UnscopedEnum.EOne & m.UnscopedEnum.EThree == m.UnscopedEnum.EOne
|
|
assert m.UnscopedEnum.EOne | m.UnscopedEnum.ETwo == m.UnscopedEnum.EThree
|
|
assert m.UnscopedEnum.EOne ^ m.UnscopedEnum.EThree == m.UnscopedEnum.ETwo
|
|
|
|
|
|
def test_scoped_enum():
|
|
assert m.test_scoped_enum(m.ScopedEnum.Three) == "ScopedEnum::Three"
|
|
z = m.ScopedEnum.Two
|
|
assert m.test_scoped_enum(z) == "ScopedEnum::Two"
|
|
|
|
# Scoped enums will *NOT* accept ==/!= int comparisons (Will always return False)
|
|
assert not z == 3
|
|
assert not 3 == z
|
|
assert z != 3
|
|
assert 3 != z
|
|
# Compare with None
|
|
assert z != None # noqa: E711
|
|
assert not (z == None) # noqa: E711
|
|
# Compare with an object
|
|
assert z != object()
|
|
assert not (z == object())
|
|
# Scoped enums will *NOT* accept >, <, >= and <= int comparisons (Will throw exceptions)
|
|
with pytest.raises(TypeError):
|
|
z > 3 # noqa: B015
|
|
with pytest.raises(TypeError):
|
|
z < 3 # noqa: B015
|
|
with pytest.raises(TypeError):
|
|
z >= 3 # noqa: B015
|
|
with pytest.raises(TypeError):
|
|
z <= 3 # noqa: B015
|
|
|
|
# order
|
|
assert m.ScopedEnum.Two < m.ScopedEnum.Three
|
|
assert m.ScopedEnum.Three > m.ScopedEnum.Two
|
|
assert m.ScopedEnum.Two <= m.ScopedEnum.Three
|
|
assert m.ScopedEnum.Two <= m.ScopedEnum.Two
|
|
assert m.ScopedEnum.Two >= m.ScopedEnum.Two
|
|
assert m.ScopedEnum.Three >= m.ScopedEnum.Two
|
|
|
|
|
|
def test_implicit_conversion():
|
|
assert str(m.ClassWithUnscopedEnum.EMode.EFirstMode) == "EMode.EFirstMode"
|
|
assert str(m.ClassWithUnscopedEnum.EFirstMode) == "EMode.EFirstMode"
|
|
assert repr(m.ClassWithUnscopedEnum.EMode.EFirstMode) == "<EMode.EFirstMode: 1>"
|
|
assert repr(m.ClassWithUnscopedEnum.EFirstMode) == "<EMode.EFirstMode: 1>"
|
|
|
|
f = m.ClassWithUnscopedEnum.test_function
|
|
first = m.ClassWithUnscopedEnum.EFirstMode
|
|
second = m.ClassWithUnscopedEnum.ESecondMode
|
|
|
|
assert f(first) == 1
|
|
|
|
assert f(first) == f(first)
|
|
assert not f(first) != f(first)
|
|
|
|
assert f(first) != f(second)
|
|
assert not f(first) == f(second)
|
|
|
|
assert f(first) == int(f(first))
|
|
assert not f(first) != int(f(first))
|
|
|
|
assert f(first) != int(f(second))
|
|
assert not f(first) == int(f(second))
|
|
|
|
# noinspection PyDictCreation
|
|
x = {f(first): 1, f(second): 2}
|
|
x[f(first)] = 3
|
|
x[f(second)] = 4
|
|
# Hashing test
|
|
assert repr(x) == "{<EMode.EFirstMode: 1>: 3, <EMode.ESecondMode: 2>: 4}"
|
|
|
|
|
|
@pytest.mark.xfail("env.GRAALPY", reason="TODO should get fixed on GraalPy side")
|
|
def test_binary_operators():
|
|
assert int(m.Flags.Read) == 4
|
|
assert int(m.Flags.Write) == 2
|
|
assert int(m.Flags.Execute) == 1
|
|
assert int(m.Flags.Read | m.Flags.Write | m.Flags.Execute) == 7
|
|
assert int(m.Flags.Read | m.Flags.Write) == 6
|
|
assert int(m.Flags.Read | m.Flags.Execute) == 5
|
|
assert int(m.Flags.Write | m.Flags.Execute) == 3
|
|
assert int(m.Flags.Write | 1) == 3
|
|
assert ~m.Flags.Write == -3
|
|
|
|
state = m.Flags.Read | m.Flags.Write
|
|
assert (state & m.Flags.Read) != 0
|
|
assert (state & m.Flags.Write) != 0
|
|
assert (state & m.Flags.Execute) == 0
|
|
assert (state & 1) == 0
|
|
|
|
state2 = ~state
|
|
assert state2 == -7
|
|
assert int(state ^ state2) == -1
|
|
|
|
|
|
def test_enum_to_int():
|
|
m.test_enum_to_int(m.Flags.Read)
|
|
m.test_enum_to_int(m.ClassWithUnscopedEnum.EMode.EFirstMode)
|
|
m.test_enum_to_int(m.ScopedCharEnum.Positive)
|
|
m.test_enum_to_int(m.ScopedBoolEnum.TRUE)
|
|
m.test_enum_to_uint(m.Flags.Read)
|
|
m.test_enum_to_uint(m.ClassWithUnscopedEnum.EMode.EFirstMode)
|
|
m.test_enum_to_uint(m.ScopedCharEnum.Positive)
|
|
m.test_enum_to_uint(m.ScopedBoolEnum.TRUE)
|
|
m.test_enum_to_long_long(m.Flags.Read)
|
|
m.test_enum_to_long_long(m.ClassWithUnscopedEnum.EMode.EFirstMode)
|
|
m.test_enum_to_long_long(m.ScopedCharEnum.Positive)
|
|
m.test_enum_to_long_long(m.ScopedBoolEnum.TRUE)
|
|
|
|
|
|
def test_duplicate_enum_name():
|
|
with pytest.raises(ValueError) as excinfo:
|
|
m.register_bad_enum()
|
|
assert str(excinfo.value) == 'SimpleEnum: element "ONE" already exists!'
|
|
|
|
|
|
def test_char_underlying_enum(): # Issue #1331/PR #1334:
|
|
assert type(m.ScopedCharEnum.Positive.__int__()) is int
|
|
assert int(m.ScopedChar16Enum.Zero) == 0
|
|
assert hash(m.ScopedChar32Enum.Positive) == 1
|
|
assert type(m.ScopedCharEnum.Positive.__getstate__()) is int
|
|
assert m.ScopedWCharEnum(1) == m.ScopedWCharEnum.Positive
|
|
with pytest.raises(TypeError):
|
|
# Even if the underlying type is char, only an int can be used to construct the enum:
|
|
m.ScopedCharEnum("0")
|
|
|
|
|
|
def test_bool_underlying_enum():
|
|
assert type(m.ScopedBoolEnum.TRUE.__int__()) is int
|
|
assert int(m.ScopedBoolEnum.FALSE) == 0
|
|
assert hash(m.ScopedBoolEnum.TRUE) == 1
|
|
assert type(m.ScopedBoolEnum.TRUE.__getstate__()) is int
|
|
assert m.ScopedBoolEnum(1) == m.ScopedBoolEnum.TRUE
|
|
# Enum could construct with a bool
|
|
# (bool is a strict subclass of int, and False will be converted to 0)
|
|
assert m.ScopedBoolEnum(False) == m.ScopedBoolEnum.FALSE
|
|
|
|
|
|
def test_docstring_signatures():
|
|
for enum_type in [m.ScopedEnum, m.UnscopedEnum]:
|
|
for attr in enum_type.__dict__.values():
|
|
# Issue #2623/PR #2637: Add argument names to enum_ methods
|
|
assert "arg0" not in (attr.__doc__ or "")
|
|
|
|
|
|
def test_str_signature():
|
|
for enum_type in [m.ScopedEnum, m.UnscopedEnum]:
|
|
assert enum_type.__str__.__doc__.startswith("__str__")
|
|
|
|
|
|
def test_generated_dunder_methods_pos_only():
|
|
for enum_type in [m.ScopedEnum, m.UnscopedEnum]:
|
|
for binary_op in [
|
|
"__eq__",
|
|
"__ne__",
|
|
"__ge__",
|
|
"__gt__",
|
|
"__lt__",
|
|
"__le__",
|
|
"__and__",
|
|
"__rand__",
|
|
# "__or__", # fail with some compilers (__doc__ = "Return self|value.")
|
|
# "__ror__", # fail with some compilers (__doc__ = "Return value|self.")
|
|
"__xor__",
|
|
"__rxor__",
|
|
"__rxor__",
|
|
]:
|
|
method = getattr(enum_type, binary_op, None)
|
|
if method is not None:
|
|
assert (
|
|
re.match(
|
|
rf"^{binary_op}\(self: [\w\.]+, other: [\w\.]+, /\)",
|
|
method.__doc__,
|
|
)
|
|
is not None
|
|
)
|
|
for unary_op in [
|
|
"__int__",
|
|
"__index__",
|
|
"__hash__",
|
|
"__str__",
|
|
"__repr__",
|
|
]:
|
|
method = getattr(enum_type, unary_op, None)
|
|
if method is not None:
|
|
assert (
|
|
re.match(
|
|
rf"^{unary_op}\(self: [\w\.]+, /\)",
|
|
method.__doc__,
|
|
)
|
|
is not None
|
|
)
|
|
assert (
|
|
re.match(
|
|
r"^__getstate__\(self: [\w\.]+, /\)",
|
|
enum_type.__getstate__.__doc__,
|
|
)
|
|
is not None
|
|
)
|
|
assert (
|
|
re.match(
|
|
r"^__setstate__\(self: [\w\.]+, state: [\w\.]+, /\)",
|
|
enum_type.__setstate__.__doc__,
|
|
)
|
|
is not None
|
|
)
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
isinstance(m.obj_cast_UnscopedEnum_ptr, str), reason=m.obj_cast_UnscopedEnum_ptr
|
|
)
|
|
def test_obj_cast_unscoped_enum_ptr():
|
|
assert m.obj_cast_UnscopedEnum_ptr(m.UnscopedEnum.ETwo) == 2
|
|
assert m.obj_cast_UnscopedEnum_ptr(m.UnscopedEnum.EOne) == 1
|
|
assert m.obj_cast_UnscopedEnum_ptr(None) == 0
|