diff --git a/.appveyor.yml b/.appveyor.yml index 391cf1071c..21ec074c5e 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -17,7 +17,7 @@ install: if ($env:PLATFORM -eq "x64") { $env:PYTHON = "$env:PYTHON-x64" } $env:PATH = "C:\Python$env:PYTHON\;C:\Python$env:PYTHON\Scripts\;$env:PATH" python -W ignore -m pip install --upgrade pip wheel - python -W ignore -m pip install pytest numpy --no-warn-script-location pytest-timeout + python -W ignore -m pip install --no-warn-script-location -r tests/requirements.txt - ps: | Start-FileDownload 'https://gitlab.com/libeigen/eigen/-/archive/3.3.7/eigen-3.3.7.zip' 7z x eigen-3.3.7.zip -y > $null diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ef554d4c2e..c9f0e26762 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -162,7 +162,7 @@ jobs: -DPYBIND11_WERROR=ON -DPYBIND11_DISABLE_HANDLE_TYPE_NAME_DEFAULT_IMPLEMENTATION=ON -DPYBIND11_SIMPLE_GIL_MANAGEMENT=ON - -DPYBIND11_PYTEST_ARGS=-v + -DPYBIND11_PYTEST_ARGS="-v -m stubgen" -DDOWNLOAD_CATCH=ON -DDOWNLOAD_EIGEN=ON -DCMAKE_CXX_STANDARD=11 @@ -190,7 +190,7 @@ jobs: cmake -S. -Bbuild2 -Werror=dev -DPYBIND11_WERROR=ON -DPYBIND11_SIMPLE_GIL_MANAGEMENT=OFF - -DPYBIND11_PYTEST_ARGS=-v + -DPYBIND11_PYTEST_ARGS="-v -m stubgen" -DDOWNLOAD_CATCH=ON -DDOWNLOAD_EIGEN=ON -DCMAKE_CXX_STANDARD=17 @@ -343,8 +343,17 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Add wget and python3 - run: apt-get update && apt-get install -y python3-dev python3-numpy python3-pytest libeigen3-dev + - name: Add python3 + run: apt-get update && apt-get install -y python3-dev libeigen3-dev + + - name: Install uv + uses: astral-sh/setup-uv@v6 + with: + activate-environment: true + enable-cache: true + + - name: Prepare env + run: uv pip install -r tests/requirements.txt - name: Configure shell: bash @@ -380,7 +389,15 @@ jobs: # tzdata will try to ask for the timezone, so set the DEBIAN_FRONTEND - name: Install 🐍 3 - run: apt-get update && DEBIAN_FRONTEND="noninteractive" apt-get install -y cmake git python3-dev python3-pytest python3-numpy + run: apt-get update && DEBIAN_FRONTEND="noninteractive" apt-get install -y cmake git python3-dev + + - name: Install uv + uses: astral-sh/setup-uv@v6 + with: + enable-cache: true + + - name: Prepare env + run: uv pip install --python=python3 --system -r tests/requirements.txt - name: Configure run: cmake -S . -B build -DPYBIND11_CUDA_TESTS=ON -DPYBIND11_WERROR=ON -DDOWNLOAD_CATCH=ON @@ -455,11 +472,11 @@ jobs: - name: Install 🐍 3 & NVHPC run: | sudo apt-get update -y && \ - sudo apt-get install -y cmake environment-modules git python3-dev python3-pip python3-numpy && \ + sudo apt-get install -y cmake environment-modules git python3-dev python3-pip && \ sudo apt-get install -y --no-install-recommends nvhpc-23-5 && \ sudo rm -rf /var/lib/apt/lists/* python3 -m pip install --upgrade pip - python3 -m pip install --upgrade pytest + python3 -m pip install -r tests/requirements.txt # On some systems, you many need further workarounds: # https://github.com/pybind/pybind11/pull/2475 @@ -506,10 +523,16 @@ jobs: - uses: actions/checkout@v4 - name: Add Python 3 - run: apt-get update; apt-get install -y python3-dev python3-numpy python3-pytest python3-pip libeigen3-dev + run: apt-get update; apt-get install -y python3-dev libeigen3-dev - - name: Update pip - run: python3 -m pip install --upgrade pip + - name: Install uv + uses: astral-sh/setup-uv@v6 + with: + activate-environment: true + enable-cache: true + + - name: Prepare env + run: uv pip install -r tests/requirements.txt - name: Update CMake uses: jwlawson/actions-setup-cmake@v2.0 @@ -724,7 +747,7 @@ jobs: run: | apt-get update apt-get install -y git make cmake g++ libeigen3-dev python3-dev python3-pip - pip3 install "pytest==6.*" + pip3 install -r tests/requirements.txt - name: Configure for install run: > @@ -992,6 +1015,11 @@ jobs: - uses: actions/checkout@v4 + - name: Prepare env + run: | + python -m pip install --break-system-packages pybind11-stubgen mypy + + - name: Configure C++11 # LTO leads to many undefined reference like # `pybind11::detail::function_call::function_call(pybind11::detail::function_call&&) diff --git a/include/pybind11/cast.h b/include/pybind11/cast.h index e3b6038028..7a6edf25b7 100644 --- a/include/pybind11/cast.h +++ b/include/pybind11/cast.h @@ -404,7 +404,7 @@ class type_caster : public type_caster { template using cast_op_type = void *&; explicit operator void *&() { return value; } - static constexpr auto name = const_name("types.CapsuleType"); + static constexpr auto name = const_name(PYBIND11_CAPSULE_TYPE_TYPE_HINT); private: void *value = nullptr; @@ -1361,7 +1361,7 @@ struct handle_type_name { }; template <> struct handle_type_name { - static constexpr auto name = const_name("typing.Union[set, frozenset]"); + static constexpr auto name = const_name("set | frozenset"); }; template <> struct handle_type_name { @@ -1441,7 +1441,7 @@ struct handle_type_name { }; template <> struct handle_type_name { - static constexpr auto name = const_name("types.CapsuleType"); + static constexpr auto name = const_name(PYBIND11_CAPSULE_TYPE_TYPE_HINT); }; template <> struct handle_type_name { @@ -1449,7 +1449,7 @@ struct handle_type_name { }; template <> struct handle_type_name { - static constexpr auto name = const_name("weakref"); + static constexpr auto name = const_name("weakref.ReferenceType"); }; template <> struct handle_type_name { diff --git a/include/pybind11/detail/common.h b/include/pybind11/detail/common.h index 4dcda799ac..7f9748853e 100644 --- a/include/pybind11/detail/common.h +++ b/include/pybind11/detail/common.h @@ -261,6 +261,15 @@ # define PYBIND11_HAS_SUBINTERPRETER_SUPPORT #endif +// 3.13 Compatibility +#if 0x030D0000 <= PY_VERSION_HEX +# define PYBIND11_TYPE_IS_TYPE_HINT "typing.TypeIs" +# define PYBIND11_CAPSULE_TYPE_TYPE_HINT "types.CapsuleType" +#else +# define PYBIND11_TYPE_IS_TYPE_HINT "typing_extensions.TypeIs" +# define PYBIND11_CAPSULE_TYPE_TYPE_HINT "typing_extensions.CapsuleType" +#endif + // 3.12 Compatibility #if 0x030C0000 <= PY_VERSION_HEX # define PYBIND11_BUFFER_TYPE_HINT "collections.abc.Buffer" @@ -268,6 +277,20 @@ # define PYBIND11_BUFFER_TYPE_HINT "typing_extensions.Buffer" #endif +// 3.11 Compatibility +#if 0x030B0000 <= PY_VERSION_HEX +# define PYBIND11_NEVER_TYPE_HINT "typing.Never" +#else +# define PYBIND11_NEVER_TYPE_HINT "typing_extensions.Never" +#endif + +// 3.10 Compatibility +#if 0x030A0000 <= PY_VERSION_HEX +# define PYBIND11_TYPE_GUARD_TYPE_HINT "typing.TypeGuard" +#else +# define PYBIND11_TYPE_GUARD_TYPE_HINT "typing_extensions.TypeGuard" +#endif + // #define PYBIND11_STR_LEGACY_PERMISSIVE // If DEFINED, pybind11::str can hold PyUnicodeObject or PyBytesObject // (probably surprising and never documented, but this was the diff --git a/include/pybind11/detail/descr.h b/include/pybind11/detail/descr.h index e5f829d2eb..701662c4cf 100644 --- a/include/pybind11/detail/descr.h +++ b/include/pybind11/detail/descr.h @@ -157,12 +157,24 @@ constexpr descr<1, Type> _() { #endif // #ifndef _ constexpr descr<0> concat() { return {}; } +constexpr descr<0> union_concat() { return {}; } template constexpr descr concat(const descr &descr) { return descr; } +template +constexpr descr union_concat(const descr &descr) { + return descr; +} + +template +constexpr descr operator|(const descr &a, + const descr &b) { + return a + const_name(" | ") + b; +} + #ifdef __cpp_fold_expressions template constexpr descr operator,(const descr &a, @@ -174,12 +186,25 @@ template constexpr auto concat(const descr &d, const Args &...args) { return (d, ..., args); } + +template +constexpr auto union_concat(const descr &d, const Args &...args) { + return (d | ... | args); +} + #else template constexpr auto concat(const descr &d, const Args &...args) -> decltype(std::declval>() + concat(args...)) { return d + const_name(", ") + concat(args...); } + +template +constexpr auto union_concat(const descr &d, const Args &...args) + -> decltype(std::declval>() + union_concat(args...)) { + return d + const_name(" | ") + union_concat(args...); +} + #endif template diff --git a/include/pybind11/stl.h b/include/pybind11/stl.h index 31cf9b20cb..01be0b47c6 100644 --- a/include/pybind11/stl.h +++ b/include/pybind11/stl.h @@ -557,8 +557,7 @@ struct optional_caster { return true; } - PYBIND11_TYPE_CASTER(Type, - const_name("typing.Optional[") + value_conv::name + const_name("]")); + PYBIND11_TYPE_CASTER(Type, value_conv::name | make_caster::name); }; #if defined(PYBIND11_HAS_OPTIONAL) @@ -642,10 +641,7 @@ struct variant_caster> { } using Type = V; - PYBIND11_TYPE_CASTER(Type, - const_name("typing.Union[") - + ::pybind11::detail::concat(make_caster::name...) - + const_name("]")); + PYBIND11_TYPE_CASTER(Type, ::pybind11::detail::union_concat(make_caster::name...)); }; #if defined(PYBIND11_HAS_VARIANT) diff --git a/include/pybind11/stl/filesystem.h b/include/pybind11/stl/filesystem.h index de64193ac4..1c0b18c1ef 100644 --- a/include/pybind11/stl/filesystem.h +++ b/include/pybind11/stl/filesystem.h @@ -96,7 +96,7 @@ struct path_caster { return true; } - PYBIND11_TYPE_CASTER(T, io_name("typing.Union[os.PathLike, str, bytes]", "pathlib.Path")); + PYBIND11_TYPE_CASTER(T, io_name("os.PathLike | str | bytes", "pathlib.Path")); }; #endif // PYBIND11_HAS_FILESYSTEM || defined(PYBIND11_HAS_EXPERIMENTAL_FILESYSTEM) diff --git a/include/pybind11/typing.h b/include/pybind11/typing.h index 57b2c087d1..1715026efa 100644 --- a/include/pybind11/typing.h +++ b/include/pybind11/typing.h @@ -218,15 +218,12 @@ struct handle_type_name> { template struct handle_type_name> { - static constexpr auto name = const_name("typing.Union[") - + ::pybind11::detail::concat(make_caster::name...) - + const_name("]"); + static constexpr auto name = ::pybind11::detail::union_concat(make_caster::name...); }; template struct handle_type_name> { - static constexpr auto name - = const_name("typing.Optional[") + make_caster::name + const_name("]"); + static constexpr auto name = make_caster::name | make_caster::name; }; template @@ -244,14 +241,14 @@ struct handle_type_name> { template struct handle_type_name> { - static constexpr auto name - = const_name("typing.TypeGuard[") + make_caster::name + const_name("]"); + static constexpr auto name = const_name(PYBIND11_TYPE_GUARD_TYPE_HINT) + const_name("[") + + make_caster::name + const_name("]"); }; template struct handle_type_name> { - static constexpr auto name - = const_name("typing.TypeIs[") + make_caster::name + const_name("]"); + static constexpr auto name = const_name(PYBIND11_TYPE_IS_TYPE_HINT) + const_name("[") + + make_caster::name + const_name("]"); }; template <> @@ -261,7 +258,7 @@ struct handle_type_name { template <> struct handle_type_name { - static constexpr auto name = const_name("typing.Never"); + static constexpr auto name = const_name(PYBIND11_NEVER_TYPE_HINT); }; #if defined(PYBIND11_TYPING_H_HAS_STRING_LITERAL) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 374a138865..e68f12c84e 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -169,6 +169,7 @@ set(PYBIND11_TEST_FILES test_smart_ptr test_stl test_stl_binders + test_stubgen test_tagbased_polymorphic test_thread test_type_caster_pyobject_ptr diff --git a/tests/conftest.py b/tests/conftest.py index 8f5352a1ff..5340484872 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -16,6 +16,7 @@ import sysconfig import textwrap import traceback +from typing import Callable import pytest @@ -242,3 +243,38 @@ def pytest_report_header(): lines.append("free-threaded Python build") return lines + + +def pytest_collection_modifyitems( + session: pytest.Session, # noqa: ARG001 + config: pytest.Config, + items: list[pytest.Item], +) -> None: + if not config.getoption("-m"): + for item in items: + if "stubgen" in item.keywords: + item.add_marker( + pytest.mark.skip(reason="Use `-m stubgen` to enable stubgen tests.") + ) + + +@pytest.fixture +def backport_typehints() -> Callable[[SanitizedString], SanitizedString]: + d = {} + if sys.version_info < (3, 13): + d["typing_extensions.TypeIs"] = "typing.TypeIs" + d["typing_extensions.CapsuleType"] = "types.CapsuleType" + if sys.version_info < (3, 12): + d["typing_extensions.Buffer"] = "collections.abc.Buffer" + if sys.version_info < (3, 11): + d["typing_extensions.Never"] = "typing.Never" + if sys.version_info < (3, 10): + d["typing_extensions.TypeGuard"] = "typing.TypeGuard" + + def backport(sanatized_string: SanitizedString) -> SanitizedString: + for old, new in d.items(): + sanatized_string.string = sanatized_string.string.replace(old, new) + + return sanatized_string + + return backport diff --git a/tests/pybind11_tests.h b/tests/pybind11_tests.h index 0eb0398df0..be6a8277cb 100644 --- a/tests/pybind11_tests.h +++ b/tests/pybind11_tests.h @@ -73,7 +73,7 @@ PYBIND11_NAMESPACE_BEGIN(detail) template <> class type_caster { public: - PYBIND11_TYPE_CASTER(RValueCaster, const_name("RValueCaster")); + PYBIND11_TYPE_CASTER(RValueCaster, const_name("typing.Literal[\"rvalue\", \"lvalue\"]")); static handle cast(RValueCaster &&, return_value_policy, handle) { return py::str("rvalue").release(); } diff --git a/tests/pyproject.toml b/tests/pyproject.toml index 469c145dfd..96733fde76 100644 --- a/tests/pyproject.toml +++ b/tests/pyproject.toml @@ -8,7 +8,7 @@ build-backend = "scikit_build_core.build" [project] name = "pybind11_tests" version = "0.0.1" -dependencies = ["pytest", "pytest-timeout", "numpy", "scipy"] +dependencies = ["pytest", "pytest-timeout", "numpy", "scipy", "pybind11-stubgen", "mypy"] [tool.scikit-build.cmake.define] PYBIND11_FINDPYTHON = true diff --git a/tests/pytest.ini b/tests/pytest.ini index 6ca7a91362..4636221289 100644 --- a/tests/pytest.ini +++ b/tests/pytest.ini @@ -20,3 +20,5 @@ filterwarnings = # bogus numpy ABI warning (see numpy/#432) ignore:.*numpy.dtype size changed.*:RuntimeWarning ignore:.*numpy.ufunc size changed.*:RuntimeWarning +markers = + stubgen: enables typing stub generation on all test modules diff --git a/tests/requirements.txt b/tests/requirements.txt index 6e3a260b19..b26a546990 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,6 +1,7 @@ --extra-index-url=https://www.graalvm.org/python/wheels --only-binary=:all: build>=1 +mypy numpy~=1.23.0; python_version=="3.8" and platform_python_implementation=="PyPy" numpy~=1.25.0; python_version=="3.9" and platform_python_implementation=="PyPy" numpy~=2.2.0; python_version=="3.10" and platform_python_implementation=="PyPy" @@ -9,6 +10,7 @@ numpy~=1.21.5; platform_python_implementation=="CPython" and python_version>="3. numpy~=1.22.2; platform_python_implementation=="CPython" and python_version=="3.10" numpy~=1.26.0; platform_python_implementation=="CPython" and python_version>="3.11" and python_version<"3.13" numpy~=2.2.0; platform_python_implementation=="CPython" and python_version=="3.13" +pybind11-stubgen pytest>=6 pytest-timeout scipy~=1.5.4; platform_python_implementation=="CPython" and python_version<"3.10" diff --git a/tests/test_buffers.py b/tests/test_buffers.py index a712f2bda4..f666df5bad 100644 --- a/tests/test_buffers.py +++ b/tests/test_buffers.py @@ -3,7 +3,6 @@ import ctypes import io import struct -import sys import pytest @@ -228,12 +227,11 @@ def test_ctypes_from_buffer(): assert not cinfo.readonly -def test_buffer_docstring(): - if sys.version_info >= (3, 12): - docstring = "get_buffer_info(arg0: collections.abc.Buffer) -> pybind11_tests.buffers.buffer_info" - else: - docstring = "get_buffer_info(arg0: typing_extensions.Buffer) -> pybind11_tests.buffers.buffer_info" - assert m.get_buffer_info.__doc__.strip() == docstring +def test_buffer_docstring(doc, backport_typehints): + assert ( + backport_typehints(doc(m.get_buffer_info)) + == "get_buffer_info(arg0: collections.abc.Buffer) -> m.buffers.buffer_info" + ) def test_buffer_exception(): diff --git a/tests/test_builtin_casters.cpp b/tests/test_builtin_casters.cpp index c516f8de7e..272d9e7d1f 100644 --- a/tests/test_builtin_casters.cpp +++ b/tests/test_builtin_casters.cpp @@ -20,7 +20,7 @@ PYBIND11_NAMESPACE_BEGIN(detail) template <> class type_caster { public: - static constexpr auto name = const_name(); + static constexpr auto name = const_name("typing.Annotated[typing.Any, \"ConstRefCasted\"]"); // Input is unimportant, a new value will always be constructed based on the // cast operator. diff --git a/tests/test_copy_move.cpp b/tests/test_copy_move.cpp index 9ac4d98d79..11ad30de8b 100644 --- a/tests/test_copy_move.cpp +++ b/tests/test_copy_move.cpp @@ -111,7 +111,7 @@ PYBIND11_NAMESPACE_BEGIN(pybind11) PYBIND11_NAMESPACE_BEGIN(detail) template <> struct type_caster { - PYBIND11_TYPE_CASTER(MoveOnlyInt, const_name("MoveOnlyInt")); + PYBIND11_TYPE_CASTER(MoveOnlyInt, const_name("typing.Annotated[int, \"MoveOnlyInt\"]")); bool load(handle src, bool) { value = MoveOnlyInt(src.cast()); return true; @@ -123,7 +123,7 @@ struct type_caster { template <> struct type_caster { - PYBIND11_TYPE_CASTER(MoveOrCopyInt, const_name("MoveOrCopyInt")); + PYBIND11_TYPE_CASTER(MoveOrCopyInt, const_name("typing.Annotated[int, \"MoveOrCopyInt\"]")); bool load(handle src, bool) { value = MoveOrCopyInt(src.cast()); return true; @@ -139,7 +139,7 @@ struct type_caster { CopyOnlyInt value; public: - static constexpr auto name = const_name("CopyOnlyInt"); + static constexpr auto name = const_name("typing.Annotated[int, \"CopyOnlyInt\"]"); bool load(handle src, bool) { value = CopyOnlyInt(src.cast()); return true; diff --git a/tests/test_custom_type_casters.cpp b/tests/test_custom_type_casters.cpp index 0ca2d25414..3529f37631 100644 --- a/tests/test_custom_type_casters.cpp +++ b/tests/test_custom_type_casters.cpp @@ -28,9 +28,9 @@ struct type_caster { public: // Classic #ifdef PYBIND11_DETAIL_UNDERSCORE_BACKWARD_COMPATIBILITY - PYBIND11_TYPE_CASTER(ArgInspector1, _("ArgInspector1")); + PYBIND11_TYPE_CASTER(ArgInspector1, _("typing.Annotated[str, \"ArgInspector1\"]")); #else - PYBIND11_TYPE_CASTER(ArgInspector1, const_name("ArgInspector1")); + PYBIND11_TYPE_CASTER(ArgInspector1, const_name("typing.Annotated[str, \"ArgInspector1\"]")); #endif bool load(handle src, bool convert) { @@ -48,7 +48,7 @@ struct type_caster { template <> struct type_caster { public: - PYBIND11_TYPE_CASTER(ArgInspector2, const_name("ArgInspector2")); + PYBIND11_TYPE_CASTER(ArgInspector2, const_name("typing.Annotated[str, \"ArgInspector2\"]")); bool load(handle src, bool convert) { value.arg = "loading ArgInspector2 argument " + std::string(convert ? "WITH" : "WITHOUT") @@ -65,7 +65,8 @@ struct type_caster { template <> struct type_caster { public: - PYBIND11_TYPE_CASTER(ArgAlwaysConverts, const_name("ArgAlwaysConverts")); + PYBIND11_TYPE_CASTER(ArgAlwaysConverts, + const_name("typing.Annotated[typing.Any, \"ArgAlwaysConverts\"]")); bool load(handle, bool convert) { return convert; } @@ -96,7 +97,8 @@ namespace PYBIND11_NAMESPACE { namespace detail { template <> struct type_caster { - PYBIND11_TYPE_CASTER(DestructionTester, const_name("DestructionTester")); + PYBIND11_TYPE_CASTER(DestructionTester, + const_name("typing.Annotated[typing.Any, \"DestructionTester\"]")); bool load(handle, bool) { return true; } static handle cast(const DestructionTester &, return_value_policy, handle) { @@ -118,7 +120,8 @@ namespace py_ = ::pybind11; // Define caster. This is effectively no-op, we only ensure it compiles and we // don't have any symbol collision when using macro mixin. struct my_caster { - PYBIND11_TYPE_CASTER(MyType, py_::detail::const_name("MyType")); + PYBIND11_TYPE_CASTER(MyType, + py_::detail::const_name("typing.Annotated[typing.Any, \"MyType\"]")); bool load(py_::handle, bool) { return true; } static py_::handle cast(const MyType &, py_::return_value_policy, py_::handle) { @@ -213,5 +216,6 @@ TEST_SUBMODULE(custom_type_casters, m) { m.def("other_lib_type", [](other_lib::MyType x) { return x; }); + py::class_(m, "ADL_issue_test"); m.def("_adl_issue", [](const ADL_issue::test &) {}); } diff --git a/tests/test_kwargs_and_defaults.cpp b/tests/test_kwargs_and_defaults.cpp index 831947f160..fa409433f6 100644 --- a/tests/test_kwargs_and_defaults.cpp +++ b/tests/test_kwargs_and_defaults.cpp @@ -297,7 +297,7 @@ TEST_SUBMODULE(kwargs_and_defaults, m) { m.def( "class_default_argument", [](py::object a) { return py::repr(std::move(a)); }, - "a"_a = py::module_::import("decimal").attr("Decimal")); + py::arg_v("a", py::module_::import("decimal").attr("Decimal"), "decimal.Decimal")); // Initial implementation of kw_only was broken when used on a method/constructor before any // other arguments diff --git a/tests/test_numpy_dtypes.cpp b/tests/test_numpy_dtypes.cpp index f206da7323..8f3ca51d69 100644 --- a/tests/test_numpy_dtypes.cpp +++ b/tests/test_numpy_dtypes.cpp @@ -354,6 +354,14 @@ TEST_SUBMODULE(numpy_dtypes, m) { // ... or after py::class_(m, "PackedStruct"); + py::class_(m, "SimpleStructReordered"); + py::class_(m, "NestedStruct"); + py::class_(m, "PartialStruct"); + py::class_(m, "PartialNestedStruct"); + py::class_(m, "StringStruct"); + py::class_(m, "ArrayStruct"); + py::class_(m, "EnumStruct"); + py::class_(m, "ComplexStruct"); PYBIND11_NUMPY_DTYPE_EX(StructWithUglyNames, __x__, "x", __y__, "y"); diff --git a/tests/test_opaque_types.py b/tests/test_opaque_types.py index 498b6e02b2..7a4d7a43da 100644 --- a/tests/test_opaque_types.py +++ b/tests/test_opaque_types.py @@ -27,7 +27,7 @@ def test_string_list(): assert m.print_opaque_list(cvp.stringList) == "Opaque list: [Element 1, Element 3]" -def test_pointers(msg): +def test_pointers(msg, backport_typehints): living_before = ConstructorStats.get(UserType).alive() assert m.get_void_ptr_value(m.return_void_ptr()) == 0x1234 assert m.get_void_ptr_value(UserType()) # Should also work for other C++ types @@ -37,14 +37,15 @@ def test_pointers(msg): with pytest.raises(TypeError) as excinfo: m.get_void_ptr_value([1, 2, 3]) # This should not work + assert ( - msg(excinfo.value) + backport_typehints(msg(excinfo.value)) == """ - get_void_ptr_value(): incompatible function arguments. The following argument types are supported: - 1. (arg0: types.CapsuleType) -> int + get_void_ptr_value(): incompatible function arguments. The following argument types are supported: + 1. (arg0: types.CapsuleType) -> int - Invoked with: [1, 2, 3] - """ + Invoked with: [1, 2, 3] + """ ) assert m.return_null_str() is None diff --git a/tests/test_pytypes.cpp b/tests/test_pytypes.cpp index 657480a005..32d635f403 100644 --- a/tests/test_pytypes.cpp +++ b/tests/test_pytypes.cpp @@ -155,7 +155,7 @@ namespace detail { template <> struct type_caster { - PYBIND11_TYPE_CASTER(RealNumber, io_name("typing.Union[float, int]", "float")); + PYBIND11_TYPE_CASTER(RealNumber, io_name("float | int", "float")); static handle cast(const RealNumber &number, return_value_policy, handle) { return py::float_(number.value).release(); diff --git a/tests/test_pytypes.py b/tests/test_pytypes.py index 545ad5b97f..a199d72f0a 100644 --- a/tests/test_pytypes.py +++ b/tests/test_pytypes.py @@ -130,10 +130,7 @@ def test_set(capture, doc): assert m.anyset_contains({"foo"}, "foo") assert doc(m.get_set) == "get_set() -> set" - assert ( - doc(m.print_anyset) - == "print_anyset(arg0: typing.Union[set, frozenset]) -> None" - ) + assert doc(m.print_anyset) == "print_anyset(arg0: set | frozenset) -> None" def test_frozenset(capture, doc): @@ -992,41 +989,37 @@ def test_type_annotation(doc): def test_union_annotations(doc): assert ( doc(m.annotate_union) - == "annotate_union(arg0: list[typing.Union[str, typing.SupportsInt, object]], arg1: str, arg2: typing.SupportsInt, arg3: object) -> list[typing.Union[str, int, object]]" + == "annotate_union(arg0: list[str | typing.SupportsInt | object], arg1: str, arg2: typing.SupportsInt, arg3: object) -> list[str | int | object]" ) def test_union_typing_only(doc): - assert ( - doc(m.union_typing_only) - == "union_typing_only(arg0: list[typing.Union[str]]) -> list[typing.Union[int]]" - ) + assert doc(m.union_typing_only) == "union_typing_only(arg0: list[str]) -> list[int]" def test_union_object_annotations(doc): assert ( doc(m.annotate_union_to_object) - == "annotate_union_to_object(arg0: typing.Union[typing.SupportsInt, str]) -> object" + == "annotate_union_to_object(arg0: typing.SupportsInt | str) -> object" ) def test_optional_annotations(doc): assert ( - doc(m.annotate_optional) - == "annotate_optional(arg0: list) -> list[typing.Optional[str]]" + doc(m.annotate_optional) == "annotate_optional(arg0: list) -> list[str | None]" ) -def test_type_guard_annotations(doc): +def test_type_guard_annotations(doc, backport_typehints): assert ( - doc(m.annotate_type_guard) + backport_typehints(doc(m.annotate_type_guard)) == "annotate_type_guard(arg0: object) -> typing.TypeGuard[str]" ) -def test_type_is_annotations(doc): +def test_type_is_annotations(doc, backport_typehints): assert ( - doc(m.annotate_type_is) + backport_typehints(doc(m.annotate_type_is)) == "annotate_type_is(arg0: object) -> typing.TypeIs[str]" ) @@ -1035,14 +1028,16 @@ def test_no_return_annotation(doc): assert doc(m.annotate_no_return) == "annotate_no_return() -> typing.NoReturn" -def test_never_annotation(doc): - assert doc(m.annotate_never) == "annotate_never() -> typing.Never" +def test_never_annotation(doc, backport_typehints): + assert ( + backport_typehints(doc(m.annotate_never)) == "annotate_never() -> typing.Never" + ) def test_optional_object_annotations(doc): assert ( doc(m.annotate_optional_to_object) - == "annotate_optional_to_object(arg0: typing.Optional[typing.SupportsInt]) -> object" + == "annotate_optional_to_object(arg0: typing.SupportsInt | None) -> object" ) @@ -1078,11 +1073,11 @@ def test_literal(doc): ) assert ( doc(m.identity_literal_arrow_with_io_name) - == 'identity_literal_arrow_with_io_name(arg0: typing.Literal["->"], arg1: typing.Union[float, int]) -> typing.Literal["->"]' + == 'identity_literal_arrow_with_io_name(arg0: typing.Literal["->"], arg1: float | int) -> typing.Literal["->"]' ) assert ( doc(m.identity_literal_arrow_with_callable) - == 'identity_literal_arrow_with_callable(arg0: collections.abc.Callable[[typing.Literal["->"], typing.Union[float, int]], float]) -> collections.abc.Callable[[typing.Literal["->"], typing.Union[float, int]], float]' + == 'identity_literal_arrow_with_callable(arg0: collections.abc.Callable[[typing.Literal["->"], float | int], float]) -> collections.abc.Callable[[typing.Literal["->"], float | int], float]' ) assert ( doc(m.identity_literal_all_special_chars) @@ -1168,9 +1163,10 @@ def test_module_attribute_types() -> None: assert module_annotations["list_int"] == "list[typing.SupportsInt]" assert module_annotations["set_str"] == "set[str]" assert module_annotations["foo"] == "pybind11_tests.pytypes.foo" + assert ( module_annotations["foo_union"] - == "typing.Union[pybind11_tests.pytypes.foo, pybind11_tests.pytypes.foo2, pybind11_tests.pytypes.foo3]" + == "pybind11_tests.pytypes.foo | pybind11_tests.pytypes.foo2 | pybind11_tests.pytypes.foo3" ) @@ -1249,14 +1245,11 @@ def test_final_annotation() -> None: assert module_annotations["CONST_INT"] == "typing.Final[int]" -def test_arg_return_type_hints(doc): - assert ( - doc(m.half_of_number) - == "half_of_number(arg0: typing.Union[float, int]) -> float" - ) +def test_arg_return_type_hints(doc, backport_typehints): + assert doc(m.half_of_number) == "half_of_number(arg0: float | int) -> float" assert ( doc(m.half_of_number_convert) - == "half_of_number_convert(x: typing.Union[float, int]) -> float" + == "half_of_number_convert(x: float | int) -> float" ) assert ( doc(m.half_of_number_noconvert) == "half_of_number_noconvert(x: float) -> float" @@ -1266,55 +1259,53 @@ def test_arg_return_type_hints(doc): assert m.half_of_number(0) == 0 assert isinstance(m.half_of_number(0), float) assert not isinstance(m.half_of_number(0), int) + # std::vector assert ( doc(m.half_of_number_vector) - == "half_of_number_vector(arg0: collections.abc.Sequence[typing.Union[float, int]]) -> list[float]" + == "half_of_number_vector(arg0: collections.abc.Sequence[float | int]) -> list[float]" ) # Tuple assert ( doc(m.half_of_number_tuple) - == "half_of_number_tuple(arg0: tuple[typing.Union[float, int], typing.Union[float, int]]) -> tuple[float, float]" + == "half_of_number_tuple(arg0: tuple[float | int, float | int]) -> tuple[float, float]" ) # Tuple assert ( doc(m.half_of_number_tuple_ellipsis) - == "half_of_number_tuple_ellipsis(arg0: tuple[typing.Union[float, int], ...]) -> tuple[float, ...]" + == "half_of_number_tuple_ellipsis(arg0: tuple[float | int, ...]) -> tuple[float, ...]" ) # Dict assert ( doc(m.half_of_number_dict) - == "half_of_number_dict(arg0: dict[str, typing.Union[float, int]]) -> dict[str, float]" + == "half_of_number_dict(arg0: dict[str, float | int]) -> dict[str, float]" ) # List assert ( doc(m.half_of_number_list) - == "half_of_number_list(arg0: list[typing.Union[float, int]]) -> list[float]" + == "half_of_number_list(arg0: list[float | int]) -> list[float]" ) # List> assert ( doc(m.half_of_number_nested_list) - == "half_of_number_nested_list(arg0: list[list[typing.Union[float, int]]]) -> list[list[float]]" + == "half_of_number_nested_list(arg0: list[list[float | int]]) -> list[list[float]]" ) # Set - assert ( - doc(m.identity_set) - == "identity_set(arg0: set[typing.Union[float, int]]) -> set[float]" - ) + assert doc(m.identity_set) == "identity_set(arg0: set[float | int]) -> set[float]" # Iterable assert ( doc(m.identity_iterable) - == "identity_iterable(arg0: collections.abc.Iterable[typing.Union[float, int]]) -> collections.abc.Iterable[float]" + == "identity_iterable(arg0: collections.abc.Iterable[float | int]) -> collections.abc.Iterable[float]" ) # Iterator assert ( doc(m.identity_iterator) - == "identity_iterator(arg0: collections.abc.Iterator[typing.Union[float, int]]) -> collections.abc.Iterator[float]" + == "identity_iterator(arg0: collections.abc.Iterator[float | int]) -> collections.abc.Iterator[float]" ) # Callable identity assert ( doc(m.identity_callable) - == "identity_callable(arg0: collections.abc.Callable[[typing.Union[float, int]], float]) -> collections.abc.Callable[[typing.Union[float, int]], float]" + == "identity_callable(arg0: collections.abc.Callable[[float | int], float]) -> collections.abc.Callable[[float | int], float]" ) # Callable identity assert ( @@ -1324,32 +1315,35 @@ def test_arg_return_type_hints(doc): # Nested Callable identity assert ( doc(m.identity_nested_callable) - == "identity_nested_callable(arg0: collections.abc.Callable[[collections.abc.Callable[[typing.Union[float, int]], float]], collections.abc.Callable[[typing.Union[float, int]], float]]) -> collections.abc.Callable[[collections.abc.Callable[[typing.Union[float, int]], float]], collections.abc.Callable[[typing.Union[float, int]], float]]" + == "identity_nested_callable(arg0: collections.abc.Callable[[collections.abc.Callable[[float | int], float]], collections.abc.Callable[[float | int], float]]) -> collections.abc.Callable[[collections.abc.Callable[[float | int], float]], collections.abc.Callable[[float | int], float]]" ) # Callable assert ( doc(m.apply_callable) - == "apply_callable(arg0: typing.Union[float, int], arg1: collections.abc.Callable[[typing.Union[float, int]], float]) -> float" + == "apply_callable(arg0: float | int, arg1: collections.abc.Callable[[float | int], float]) -> float" ) # Callable assert ( doc(m.apply_callable_ellipsis) - == "apply_callable_ellipsis(arg0: typing.Union[float, int], arg1: collections.abc.Callable[..., float]) -> float" + == "apply_callable_ellipsis(arg0: float | int, arg1: collections.abc.Callable[..., float]) -> float" ) # Union assert ( doc(m.identity_union) - == "identity_union(arg0: typing.Union[typing.Union[float, int], str]) -> typing.Union[float, str]" + == "identity_union(arg0: float | int | str) -> float | str" ) # Optional assert ( doc(m.identity_optional) - == "identity_optional(arg0: typing.Optional[typing.Union[float, int]]) -> typing.Optional[float]" + == "identity_optional(arg0: float | int | None) -> float | None" + ) + # TypeIs + assert ( + backport_typehints(doc(m.check_type_is)) + == "check_type_is(arg0: object) -> typing.TypeIs[float]" ) # TypeGuard assert ( - doc(m.check_type_guard) + backport_typehints(doc(m.check_type_guard)) == "check_type_guard(arg0: list[object]) -> typing.TypeGuard[list[float]]" ) - # TypeIs - assert doc(m.check_type_is) == "check_type_is(arg0: object) -> typing.TypeIs[float]" diff --git a/tests/test_stl.cpp b/tests/test_stl.cpp index 5e6d6a333f..75e633290f 100644 --- a/tests/test_stl.cpp +++ b/tests/test_stl.cpp @@ -550,6 +550,7 @@ TEST_SUBMODULE(stl, m) { // #528: templated constructor // (no python tests: the test here is that this compiles) + py::class_(m, "TplCtorClass"); m.def("tpl_ctor_vector", [](std::vector &) {}); m.def("tpl_ctor_map", [](std::unordered_map &) {}); m.def("tpl_ctor_set", [](std::unordered_set &) {}); diff --git a/tests/test_stl.py b/tests/test_stl.py index c952d034fe..4a57635e27 100644 --- a/tests/test_stl.py +++ b/tests/test_stl.py @@ -227,11 +227,16 @@ def test_boost_optional(): assert int(props.access_by_copy) == 42 -def test_reference_sensitive_optional(): +def test_reference_sensitive_optional(doc): assert m.double_or_zero_refsensitive(None) == 0 assert m.double_or_zero_refsensitive(42) == 84 pytest.raises(TypeError, m.double_or_zero_refsensitive, "foo") + assert ( + doc(m.double_or_zero_refsensitive) + == "double_or_zero_refsensitive(arg0: typing.SupportsInt | None) -> int" + ) + assert m.half_or_none_refsensitive(0) is None assert m.half_or_none_refsensitive(42) == 21 pytest.raises(TypeError, m.half_or_none_refsensitive, "foo") @@ -257,7 +262,7 @@ def test_reference_sensitive_optional(): @pytest.mark.skipif(not hasattr(m, "has_filesystem"), reason="no ") -def test_fs_path(doc): +def test_fs_path(): from pathlib import Path class PseudoStrPath: @@ -274,37 +279,17 @@ def __fspath__(self): assert m.parent_path(b"foo/bar") == Path("foo") assert m.parent_path(PseudoStrPath()) == Path("foo") assert m.parent_path(PseudoBytesPath()) == Path("foo") - assert ( - doc(m.parent_path) - == "parent_path(arg0: typing.Union[os.PathLike, str, bytes]) -> pathlib.Path" - ) # std::vector assert m.parent_paths(["foo/bar", "foo/baz"]) == [Path("foo"), Path("foo")] - assert ( - doc(m.parent_paths) - == "parent_paths(arg0: collections.abc.Sequence[typing.Union[os.PathLike, str, bytes]]) -> list[pathlib.Path]" - ) # py::typing::List assert m.parent_paths_list(["foo/bar", "foo/baz"]) == [Path("foo"), Path("foo")] - assert ( - doc(m.parent_paths_list) - == "parent_paths_list(arg0: list[typing.Union[os.PathLike, str, bytes]]) -> list[pathlib.Path]" - ) # Nested py::typing::List assert m.parent_paths_nested_list([["foo/bar"], ["foo/baz", "foo/buzz"]]) == [ [Path("foo")], [Path("foo"), Path("foo")], ] - assert ( - doc(m.parent_paths_nested_list) - == "parent_paths_nested_list(arg0: list[list[typing.Union[os.PathLike, str, bytes]]]) -> list[list[pathlib.Path]]" - ) # py::typing::Tuple assert m.parent_paths_tuple(("foo/bar", "foo/baz")) == (Path("foo"), Path("foo")) - assert ( - doc(m.parent_paths_tuple) - == "parent_paths_tuple(arg0: tuple[typing.Union[os.PathLike, str, bytes], typing.Union[os.PathLike, str, bytes]]) -> tuple[pathlib.Path, pathlib.Path]" - ) # py::typing::Dict assert m.parent_paths_dict( { @@ -317,9 +302,39 @@ def __fspath__(self): "key2": Path("foo"), "key3": Path("foo"), } + + +@pytest.mark.skipif(not hasattr(m, "has_filesystem"), reason="no ") +def test_path_typing(doc): + # Single argument + assert ( + doc(m.parent_path) + == "parent_path(arg0: os.PathLike | str | bytes) -> pathlib.Path" + ) + # std::vector + assert ( + doc(m.parent_paths) + == "parent_paths(arg0: collections.abc.Sequence[os.PathLike | str | bytes]) -> list[pathlib.Path]" + ) + # py::typing::List + assert ( + doc(m.parent_paths_list) + == "parent_paths_list(arg0: list[os.PathLike | str | bytes]) -> list[pathlib.Path]" + ) + # Nested py::typing::List + assert ( + doc(m.parent_paths_nested_list) + == "parent_paths_nested_list(arg0: list[list[os.PathLike | str | bytes]]) -> list[list[pathlib.Path]]" + ) + # py::typing::Tuple + assert ( + doc(m.parent_paths_tuple) + == "parent_paths_tuple(arg0: tuple[os.PathLike | str | bytes, os.PathLike | str | bytes]) -> tuple[pathlib.Path, pathlib.Path]" + ) + # py::typing::Dict assert ( doc(m.parent_paths_dict) - == "parent_paths_dict(arg0: dict[str, typing.Union[os.PathLike, str, bytes]]) -> dict[str, pathlib.Path]" + == "parent_paths_dict(arg0: dict[str, os.PathLike | str | bytes]) -> dict[str, pathlib.Path]" ) @@ -337,7 +352,7 @@ def test_variant(doc): assert ( doc(m.load_variant) - == "load_variant(arg0: typing.Union[typing.SupportsInt, str, typing.SupportsFloat, None]) -> str" + == "load_variant(arg0: typing.SupportsInt | str | typing.SupportsFloat | None) -> str" ) @@ -353,7 +368,7 @@ def test_variant_monostate(doc): assert ( doc(m.load_monostate_variant) - == "load_monostate_variant(arg0: typing.Union[None, typing.SupportsInt, str]) -> str" + == "load_monostate_variant(arg0: None | typing.SupportsInt | str) -> str" ) diff --git a/tests/test_stubgen.cpp b/tests/test_stubgen.cpp new file mode 100644 index 0000000000..a591b8de2b --- /dev/null +++ b/tests/test_stubgen.cpp @@ -0,0 +1,5 @@ +#include "pybind11_tests.h" + +TEST_SUBMODULE(stubgen, m) { + m.def("add_int", [](int a, int b) { return a + b; }, "a"_a, "b"_a); +} diff --git a/tests/test_stubgen.py b/tests/test_stubgen.py new file mode 100644 index 0000000000..0907b1aad9 --- /dev/null +++ b/tests/test_stubgen.py @@ -0,0 +1,76 @@ +from __future__ import annotations + +from pathlib import Path +from typing import NamedTuple + +import pybind11_stubgen +import pytest +from mypy import api + +from pybind11_tests import stubgen as m + + +class MypyResult(NamedTuple): + normal_report: str + error_report: str + exit_status: int + + +def run_mypy(stubs: Path) -> MypyResult: + """Run mypy on the given stubs directory.""" + normal_report, error_report, exit_status = api.run( + [stubs.as_posix(), "--no-color-output"] + ) + print("Normal report:") + print(normal_report) + print("Error report:") + print(error_report) + return MypyResult(normal_report, error_report, exit_status) + + +def test_stubgen(tmp_path: Path) -> None: + assert m.add_int(1, 2) == 3 + # Generate stub into temporary directory + pybind11_stubgen.main( + [ + "pybind11_tests.stubgen", + "-o", + tmp_path.as_posix(), + ] + ) + # Check stub file is generated and contains expected content + stub_file = tmp_path / "pybind11_tests" / "stubgen.pyi" + assert stub_file.exists() + stub_content = stub_file.read_text() + assert ( + "def add_int(a: typing.SupportsInt, b: typing.SupportsInt) -> int:" + in stub_content + ) + # Run mypy on the generated stub file + result = run_mypy(stub_file) + assert result.exit_status == 0 + assert "Success: no issues found in 1 source file" in result.normal_report + + +@pytest.mark.stubgen +def test_stubgen_all(tmp_path: Path, caplog: pytest.LogCaptureFixture) -> None: + # Generate stub into temporary directory + pybind11_stubgen.main( + [ + "pybind11_tests", + "-o", + tmp_path.as_posix(), + ] + ) + # Errors are reported using logging + assert ( + "Raw C++ types/values were found in signatures extracted from docstrings" + in caplog.text + ) + # Check stub file is generated and contains expected content + stubs = tmp_path / "pybind11_tests" + assert stubs.exists() + # Run mypy on the generated stub file + result = run_mypy(stubs) + assert result.exit_status == 0 + assert "Success: no issues found in 1 source file" in result.normal_report diff --git a/tests/test_virtual_functions.cpp b/tests/test_virtual_functions.cpp index a6164eb81d..9a496b59b0 100644 --- a/tests/test_virtual_functions.cpp +++ b/tests/test_virtual_functions.cpp @@ -324,12 +324,11 @@ TEST_SUBMODULE(virtual_functions, m) { // test_recursive_dispatch_issue // #3357: Recursive dispatch fails to find python function override + pybind11::class_(m, "Data").def(pybind11::init<>()); pybind11::class_(m, "Adder") .def(pybind11::init<>()) .def("__call__", &AdderBase::operator()); - pybind11::class_(m, "Data").def(pybind11::init<>()); - m.def("add2", [](const AdderBase::Data &first, const AdderBase::Data &second,