diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2951a0ad19..1503a25656 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -176,6 +176,11 @@ jobs: run: pytest tests/extra_setuptools if: "!(matrix.runs-on == 'windows-2022')" + # This tests ABI compatibility + - name: ABI test + if: matrix.python != '3.11-dev' + run: pytest tests/extra_abi + deadsnakes: strategy: diff --git a/.gitignore b/.gitignore index 43d5094c96..0ef3180c74 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,4 @@ pybind11Targets.cmake /docs/_build/* .ipynb_checkpoints/ tests/main.cpp +tests/*/temp* diff --git a/noxfile.py b/noxfile.py index 021ced2453..3e2b58f777 100644 --- a/noxfile.py +++ b/noxfile.py @@ -61,6 +61,23 @@ def tests_packaging(session: nox.Session) -> None: session.run("pytest", "tests/extra_python_package", *session.posargs) +@nox.session +def tests_abi(session: nox.Session) -> None: + """ + Run the abi checker. + """ + + session.install("pybind11==2.8.0", "--no-build-isolation") + session.install( + "tests/extra_abi", "--no-build-isolation", env={"EXAMPLE_NAME": "pet"} + ) + session.install(".", "--no-build-isolation") + session.install( + "tests/extra_abi", "--no-build-isolation", env={"EXAMPLE_NAME": "dog"} + ) + session.run("python", "tests/extra_abi/check_installed.py") + + @nox.session(reuse_venv=True) def docs(session: nox.Session) -> None: """ diff --git a/tests/conftest.py b/tests/conftest.py index 02ce263afc..517e0908f1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -13,7 +13,10 @@ import pytest # Early diagnostic for failed imports -import pybind11_tests +try: + import pybind11_tests +except ModuleNotFoundError: + pybind11_tests = None _long_marker = re.compile(r"([0-9])L") _hexadecimal = re.compile(r"0x[0-9a-fA-F]+") @@ -201,13 +204,14 @@ def pytest_configure(): def pytest_report_header(config): - del config # Unused. - assert ( - pybind11_tests.compiler_info is not None - ), "Please update pybind11_tests.cpp if this assert fails." - return ( - "C++ Info:" - f" {pybind11_tests.compiler_info}" - f" {pybind11_tests.cpp_std}" - f" {pybind11_tests.PYBIND11_INTERNALS_ID}" - ) + if pybind11_tests is not None: + del config # Unused. + assert ( + pybind11_tests.compiler_info is not None + ), "Please update pybind11_tests.cpp if this assert fails." + return ( + "C++ Info:" + f" {pybind11_tests.compiler_info}" + f" {pybind11_tests.cpp_std}" + f" {pybind11_tests.PYBIND11_INTERNALS_ID}" + ) diff --git a/tests/extra_abi/check_installed.py b/tests/extra_abi/check_installed.py new file mode 100755 index 0000000000..ca8fe7fa80 --- /dev/null +++ b/tests/extra_abi/check_installed.py @@ -0,0 +1,13 @@ +try: + import dog + + raise RuntimeError("Broken! Dog must require Pet to be loaded") +except ImportError: + import pet # noqa: F401 + +import dog # noqa: F811 + +d = dog.Dog("Bluey") + +d.bark() +assert d.name == "Bluey" diff --git a/tests/extra_abi/dog.cpp b/tests/extra_abi/dog.cpp new file mode 100644 index 0000000000..da40d5885e --- /dev/null +++ b/tests/extra_abi/dog.cpp @@ -0,0 +1,11 @@ +#include + +#include "dog.hpp" + +#include + +namespace py = pybind11; + +PYBIND11_MODULE(dog, m) { + py::class_(m, "Dog").def(py::init()).def("bark", &Dog::bark); +} diff --git a/tests/extra_abi/dog.hpp b/tests/extra_abi/dog.hpp new file mode 100644 index 0000000000..f604716a9a --- /dev/null +++ b/tests/extra_abi/dog.hpp @@ -0,0 +1,10 @@ +#include + +#include "pet.hpp" + +#include + +struct PYBIND11_EXPORT Dog : Pet { + explicit Dog(const std::string &name) : Pet(name) {} + std::string bark() const { return "woof!"; } +}; diff --git a/tests/extra_abi/pet.cpp b/tests/extra_abi/pet.cpp new file mode 100644 index 0000000000..7830b90e24 --- /dev/null +++ b/tests/extra_abi/pet.cpp @@ -0,0 +1,12 @@ +#include + +#include "pet.hpp" + +#include + +namespace py = pybind11; + +PYBIND11_MODULE(pet, m) { + py::class_ pet(m, "Pet"); + pet.def(py::init()).def_readwrite("name", &Pet::name); +} diff --git a/tests/extra_abi/pet.hpp b/tests/extra_abi/pet.hpp new file mode 100644 index 0000000000..37fa90b186 --- /dev/null +++ b/tests/extra_abi/pet.hpp @@ -0,0 +1,6 @@ +#include + +struct Pet { + explicit Pet(const std::string &name) : name(name) {} + std::string name; +}; diff --git a/tests/extra_abi/setup.py b/tests/extra_abi/setup.py new file mode 100644 index 0000000000..809be19633 --- /dev/null +++ b/tests/extra_abi/setup.py @@ -0,0 +1,20 @@ +import os +import sys + +from setuptools import setup + +from pybind11.setup_helpers import Pybind11Extension + +name = os.environ["EXAMPLE_NAME"] +assert name in {"pet", "dog"} + + +ext = Pybind11Extension( + name, + [f"{name}.cpp"], + include_dirs=["."], + cxx_std=11, + extra_compile_args=["/d2FH4-"] if sys.platform.startswith("win32") else [], +) + +setup(name=name, version="0.0.0", ext_modules=[ext]) diff --git a/tests/extra_abi/test_venv_abi.py b/tests/extra_abi/test_venv_abi.py new file mode 100755 index 0000000000..95d368865d --- /dev/null +++ b/tests/extra_abi/test_venv_abi.py @@ -0,0 +1,16 @@ +from pathlib import Path + +DIR = Path(__file__).parent.resolve() + + +def test_build_import(virtualenv): + virtualenv.run('pip install "pybind11==2.8.0" --no-build-isolation') + virtualenv.env["EXAMPLE_NAME"] = "pet" + virtualenv.run(f"pip install {DIR} --no-build-isolation") + + virtualenv.run(f"pip install {DIR.parent.parent} --no-build-isolation") + virtualenv.env["EXAMPLE_NAME"] = "dog" + virtualenv.run(f"pip install {DIR} --no-build-isolation") + + script = DIR / "check_installed.py" + virtualenv.run(f"python {script}") diff --git a/tests/requirements.txt b/tests/requirements.txt index 04aafa8cf9..d8ad4643fb 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -5,5 +5,6 @@ numpy==1.21.5; platform_python_implementation!="PyPy" and python_version>="3.7" numpy==1.22.2; platform_python_implementation!="PyPy" and python_version>="3.10" and python_version<"3.11" pytest==7.0.0 pytest-timeout +pytest-virtualenv scipy==1.5.4; platform_python_implementation!="PyPy" and python_version<"3.10" scipy==1.8.0; platform_python_implementation!="PyPy" and python_version=="3.10"