mirror of
https://github.com/pybind/pybind11.git
synced 2026-03-14 20:27:47 +00:00
* 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>
355 lines
12 KiB
Python
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
|