mirror of
https://github.com/pybind/pybind11.git
synced 2026-04-19 14:29:11 +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.
316 lines
9.2 KiB
Python
316 lines
9.2 KiB
Python
from __future__ import annotations
|
|
|
|
import contextlib
|
|
import os
|
|
import string
|
|
import subprocess
|
|
import sys
|
|
import tarfile
|
|
import zipfile
|
|
|
|
# These tests must be run explicitly
|
|
|
|
DIR = os.path.abspath(os.path.dirname(__file__))
|
|
MAIN_DIR = os.path.dirname(os.path.dirname(DIR))
|
|
|
|
PKGCONFIG = """\
|
|
prefix=${{pcfiledir}}/../../
|
|
includedir=${{prefix}}/include
|
|
|
|
Name: pybind11
|
|
Description: Seamless operability between C++11 and Python
|
|
Version: {VERSION}
|
|
Cflags: -I${{includedir}}
|
|
"""
|
|
|
|
|
|
main_headers = {
|
|
"include/pybind11/attr.h",
|
|
"include/pybind11/buffer_info.h",
|
|
"include/pybind11/cast.h",
|
|
"include/pybind11/chrono.h",
|
|
"include/pybind11/common.h",
|
|
"include/pybind11/complex.h",
|
|
"include/pybind11/eigen.h",
|
|
"include/pybind11/embed.h",
|
|
"include/pybind11/eval.h",
|
|
"include/pybind11/functional.h",
|
|
"include/pybind11/gil.h",
|
|
"include/pybind11/gil_safe_call_once.h",
|
|
"include/pybind11/iostream.h",
|
|
"include/pybind11/native_enum.h",
|
|
"include/pybind11/numpy.h",
|
|
"include/pybind11/operators.h",
|
|
"include/pybind11/options.h",
|
|
"include/pybind11/pybind11.h",
|
|
"include/pybind11/pytypes.h",
|
|
"include/pybind11/stl.h",
|
|
"include/pybind11/stl_bind.h",
|
|
"include/pybind11/trampoline_self_life_support.h",
|
|
"include/pybind11/type_caster_pyobject_ptr.h",
|
|
"include/pybind11/typing.h",
|
|
"include/pybind11/warnings.h",
|
|
}
|
|
|
|
conduit_headers = {
|
|
"include/pybind11/conduit/README.txt",
|
|
"include/pybind11/conduit/pybind11_conduit_v1.h",
|
|
"include/pybind11/conduit/pybind11_platform_abi_id.h",
|
|
"include/pybind11/conduit/wrap_include_python_h.h",
|
|
}
|
|
|
|
detail_headers = {
|
|
"include/pybind11/detail/class.h",
|
|
"include/pybind11/detail/common.h",
|
|
"include/pybind11/detail/cpp_conduit.h",
|
|
"include/pybind11/detail/descr.h",
|
|
"include/pybind11/detail/dynamic_raw_ptr_cast_if_possible.h",
|
|
"include/pybind11/detail/init.h",
|
|
"include/pybind11/detail/internals.h",
|
|
"include/pybind11/detail/native_enum_data.h",
|
|
"include/pybind11/detail/struct_smart_holder.h",
|
|
"include/pybind11/detail/type_caster_base.h",
|
|
"include/pybind11/detail/typeid.h",
|
|
"include/pybind11/detail/using_smart_holder.h",
|
|
"include/pybind11/detail/value_and_holder.h",
|
|
"include/pybind11/detail/exception_translation.h",
|
|
}
|
|
|
|
eigen_headers = {
|
|
"include/pybind11/eigen/common.h",
|
|
"include/pybind11/eigen/matrix.h",
|
|
"include/pybind11/eigen/tensor.h",
|
|
}
|
|
|
|
stl_headers = {
|
|
"include/pybind11/stl/filesystem.h",
|
|
}
|
|
|
|
cmake_files = {
|
|
"share/cmake/pybind11/FindPythonLibsNew.cmake",
|
|
"share/cmake/pybind11/pybind11Common.cmake",
|
|
"share/cmake/pybind11/pybind11Config.cmake",
|
|
"share/cmake/pybind11/pybind11ConfigVersion.cmake",
|
|
"share/cmake/pybind11/pybind11GuessPythonExtSuffix.cmake",
|
|
"share/cmake/pybind11/pybind11NewTools.cmake",
|
|
"share/cmake/pybind11/pybind11Targets.cmake",
|
|
"share/cmake/pybind11/pybind11Tools.cmake",
|
|
}
|
|
|
|
pkgconfig_files = {
|
|
"share/pkgconfig/pybind11.pc",
|
|
}
|
|
|
|
py_files = {
|
|
"__init__.py",
|
|
"__main__.py",
|
|
"_version.py",
|
|
"commands.py",
|
|
"py.typed",
|
|
"setup_helpers.py",
|
|
"share/__init__.py",
|
|
"share/pkgconfig/__init__.py",
|
|
}
|
|
|
|
headers = main_headers | conduit_headers | detail_headers | eigen_headers | stl_headers
|
|
src_files = headers | cmake_files | pkgconfig_files
|
|
all_files = src_files | py_files
|
|
|
|
|
|
sdist_files = {
|
|
"pybind11",
|
|
"pybind11/include",
|
|
"pybind11/include/pybind11",
|
|
"pybind11/include/pybind11/conduit",
|
|
"pybind11/include/pybind11/detail",
|
|
"pybind11/include/pybind11/eigen",
|
|
"pybind11/include/pybind11/stl",
|
|
"pybind11/share",
|
|
"pybind11/share/cmake",
|
|
"pybind11/share/cmake/pybind11",
|
|
"pybind11/share/pkgconfig",
|
|
"pyproject.toml",
|
|
"setup.cfg",
|
|
"setup.py",
|
|
"LICENSE",
|
|
"MANIFEST.in",
|
|
"README.rst",
|
|
"PKG-INFO",
|
|
"SECURITY.md",
|
|
}
|
|
|
|
local_sdist_files = {
|
|
".egg-info",
|
|
".egg-info/PKG-INFO",
|
|
".egg-info/SOURCES.txt",
|
|
".egg-info/dependency_links.txt",
|
|
".egg-info/not-zip-safe",
|
|
".egg-info/top_level.txt",
|
|
}
|
|
|
|
|
|
def read_tz_file(tar: tarfile.TarFile, name: str) -> bytes:
|
|
start = tar.getnames()[0] + "/"
|
|
inner_file = tar.extractfile(tar.getmember(f"{start}{name}"))
|
|
assert inner_file
|
|
with contextlib.closing(inner_file) as f:
|
|
return f.read()
|
|
|
|
|
|
def normalize_line_endings(value: bytes) -> bytes:
|
|
return value.replace(os.linesep.encode("utf-8"), b"\n")
|
|
|
|
|
|
def test_build_sdist(monkeypatch, tmpdir):
|
|
monkeypatch.chdir(MAIN_DIR)
|
|
|
|
subprocess.run(
|
|
[sys.executable, "-m", "build", "--sdist", f"--outdir={tmpdir}"], check=True
|
|
)
|
|
|
|
(sdist,) = tmpdir.visit("*.tar.gz")
|
|
|
|
with tarfile.open(str(sdist), "r:gz") as tar:
|
|
start = tar.getnames()[0] + "/"
|
|
version = start[9:-1]
|
|
simpler = {n.split("/", 1)[-1] for n in tar.getnames()[1:]}
|
|
|
|
setup_py = read_tz_file(tar, "setup.py")
|
|
pyproject_toml = read_tz_file(tar, "pyproject.toml")
|
|
pkgconfig = read_tz_file(tar, "pybind11/share/pkgconfig/pybind11.pc")
|
|
cmake_cfg = read_tz_file(
|
|
tar, "pybind11/share/cmake/pybind11/pybind11Config.cmake"
|
|
)
|
|
|
|
assert (
|
|
'set(pybind11_INCLUDE_DIR "${PACKAGE_PREFIX_DIR}/include")'
|
|
in cmake_cfg.decode("utf-8")
|
|
)
|
|
|
|
files = {f"pybind11/{n}" for n in all_files}
|
|
files |= sdist_files
|
|
files |= {f"pybind11{n}" for n in local_sdist_files}
|
|
files.add("pybind11.egg-info/entry_points.txt")
|
|
files.add("pybind11.egg-info/requires.txt")
|
|
assert simpler == files
|
|
|
|
with open(os.path.join(MAIN_DIR, "tools", "setup_main.py.in"), "rb") as f:
|
|
contents = (
|
|
string.Template(f.read().decode("utf-8"))
|
|
.substitute(version=version, extra_cmd="")
|
|
.encode("utf-8")
|
|
)
|
|
assert setup_py == contents
|
|
|
|
with open(os.path.join(MAIN_DIR, "tools", "pyproject.toml"), "rb") as f:
|
|
contents = f.read()
|
|
assert pyproject_toml == contents
|
|
|
|
simple_version = ".".join(version.split(".")[:3])
|
|
pkgconfig_expected = PKGCONFIG.format(VERSION=simple_version).encode("utf-8")
|
|
assert normalize_line_endings(pkgconfig) == pkgconfig_expected
|
|
|
|
|
|
def test_build_global_dist(monkeypatch, tmpdir):
|
|
monkeypatch.chdir(MAIN_DIR)
|
|
monkeypatch.setenv("PYBIND11_GLOBAL_SDIST", "1")
|
|
subprocess.run(
|
|
[sys.executable, "-m", "build", "--sdist", "--outdir", str(tmpdir)], check=True
|
|
)
|
|
|
|
(sdist,) = tmpdir.visit("*.tar.gz")
|
|
|
|
with tarfile.open(str(sdist), "r:gz") as tar:
|
|
start = tar.getnames()[0] + "/"
|
|
version = start[16:-1]
|
|
simpler = {n.split("/", 1)[-1] for n in tar.getnames()[1:]}
|
|
|
|
setup_py = read_tz_file(tar, "setup.py")
|
|
pyproject_toml = read_tz_file(tar, "pyproject.toml")
|
|
pkgconfig = read_tz_file(tar, "pybind11/share/pkgconfig/pybind11.pc")
|
|
cmake_cfg = read_tz_file(
|
|
tar, "pybind11/share/cmake/pybind11/pybind11Config.cmake"
|
|
)
|
|
|
|
assert (
|
|
'set(pybind11_INCLUDE_DIR "${PACKAGE_PREFIX_DIR}/include")'
|
|
in cmake_cfg.decode("utf-8")
|
|
)
|
|
|
|
files = {f"pybind11/{n}" for n in all_files}
|
|
files |= sdist_files
|
|
files |= {f"pybind11_global{n}" for n in local_sdist_files}
|
|
assert simpler == files
|
|
|
|
with open(os.path.join(MAIN_DIR, "tools", "setup_global.py.in"), "rb") as f:
|
|
contents = (
|
|
string.Template(f.read().decode())
|
|
.substitute(version=version, extra_cmd="")
|
|
.encode("utf-8")
|
|
)
|
|
assert setup_py == contents
|
|
|
|
with open(os.path.join(MAIN_DIR, "tools", "pyproject.toml"), "rb") as f:
|
|
contents = f.read()
|
|
assert pyproject_toml == contents
|
|
|
|
simple_version = ".".join(version.split(".")[:3])
|
|
pkgconfig_expected = PKGCONFIG.format(VERSION=simple_version).encode("utf-8")
|
|
assert normalize_line_endings(pkgconfig) == pkgconfig_expected
|
|
|
|
|
|
def tests_build_wheel(monkeypatch, tmpdir):
|
|
monkeypatch.chdir(MAIN_DIR)
|
|
|
|
subprocess.run(
|
|
[sys.executable, "-m", "pip", "wheel", ".", "-w", str(tmpdir)], check=True
|
|
)
|
|
|
|
(wheel,) = tmpdir.visit("*.whl")
|
|
|
|
files = {f"pybind11/{n}" for n in all_files}
|
|
files |= {
|
|
"dist-info/LICENSE",
|
|
"dist-info/METADATA",
|
|
"dist-info/RECORD",
|
|
"dist-info/WHEEL",
|
|
"dist-info/entry_points.txt",
|
|
"dist-info/top_level.txt",
|
|
}
|
|
|
|
with zipfile.ZipFile(str(wheel)) as z:
|
|
names = z.namelist()
|
|
|
|
trimmed = {n for n in names if "dist-info" not in n}
|
|
trimmed |= {f"dist-info/{n.split('/', 1)[-1]}" for n in names if "dist-info" in n}
|
|
assert files == trimmed
|
|
|
|
|
|
def tests_build_global_wheel(monkeypatch, tmpdir):
|
|
monkeypatch.chdir(MAIN_DIR)
|
|
monkeypatch.setenv("PYBIND11_GLOBAL_SDIST", "1")
|
|
|
|
subprocess.run(
|
|
[sys.executable, "-m", "pip", "wheel", ".", "-w", str(tmpdir)], check=True
|
|
)
|
|
|
|
(wheel,) = tmpdir.visit("*.whl")
|
|
|
|
files = {f"data/data/{n}" for n in src_files}
|
|
files |= {f"data/headers/{n[8:]}" for n in headers}
|
|
files |= {
|
|
"dist-info/LICENSE",
|
|
"dist-info/METADATA",
|
|
"dist-info/WHEEL",
|
|
"dist-info/top_level.txt",
|
|
"dist-info/RECORD",
|
|
}
|
|
|
|
with zipfile.ZipFile(str(wheel)) as z:
|
|
names = z.namelist()
|
|
|
|
beginning = names[0].split("/", 1)[0].rsplit(".", 1)[0]
|
|
trimmed = {n[len(beginning) + 1 :] for n in names}
|
|
|
|
assert files == trimmed
|