Files
pybind11/tests/test_enum.py
Scott Wolchok ccb7129f54 Improve performance of enum_ operators by going back to specific implementation (#5887)
* Improve performance of enum_ operators by going back to specific implementation

test_enum needs a patch because ops are now overloaded and this affects their docstrings.

* outline call_impl to save on code size

This does cause more move constructions, as shown by the needed update to test_copy_move. Up to reviewers whether they want more code size or more moves.

* add function_ref.h to PYBIND11_HEADERS.

* Update test_copy_move tests with C++17 passing values just so we can see mostly-not-red tests

* Remove stray TODO

* fix clang-tidy

* fix clang-tidy again. add function_ref.h to test_files.py

* Add static assertion for function_ref lifetime safety in call_impl

Add a static_assert to document and enforce that function_ref is
trivially copyable, ensuring safe pass-by-value usage. This also
documents the lifetime safety guarantees: function_ref is created
from cap->f which lives in the capture object, and is only used
synchronously within call_impl without being stored beyond its scope.

* Add #undef cleanup for enum operator macros

Undefine all enum operator macros after their last use to prevent
macro pollution and follow the existing code pattern. This matches
the cleanup pattern used for the previous enum operator macros.

* Rename PYBIND11_THROW to PYBIND11_ENUM_OP_THROW_TYPE_ERROR

Rename the macro to be more specific and avoid potential clashes with
public macros. The new name clearly indicates it's scoped to enum
operations and describes its purpose (throwing a type error).

* Clarify comments in function_ref.h

Replace vague comments about 'extensions to <functional>' and 'functions'
with a clearer description that this is a header-only class template
similar to std::function but with non-owning semantics. This makes it
clear that it's template-only and requires no additional library linking.

---------

Co-authored-by: Ralf W. Grosse-Kunstleve <rgrossekunst@nvidia.com>
2026-02-16 23:00:21 -08:00

355 lines
12 KiB
Python

# ruff: noqa: SIM201 SIM300 SIM202
from __future__ import annotations
import re
import pytest
import env
from pybind11_tests import enums as m
@pytest.mark.xfail(
env.GRAALPY and env.GRAALPY_VERSION < (24, 2), reason="Fixed in GraalPy 24.2"
)
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",
]:
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 and env.GRAALPY_VERSION < (24, 2), reason="Fixed in GraalPy 24.2"
)
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:
# 1) The docs must start with the name of the op.
assert (
re.match(
rf"^{binary_op}\(",
method.__doc__,
)
is not None
)
# 2) The docs must contain the op's signature. This is a separate check
# and not anchored at the start because the op may be overloaded.
assert (
re.search(
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