diff --git a/tests/packages/importlib_editable/CMakeLists.txt b/tests/packages/importlib_editable/CMakeLists.txt new file mode 100644 index 00000000..7a96fb06 --- /dev/null +++ b/tests/packages/importlib_editable/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.15...3.26) +project(${SKBUILD_PROJECT_NAME} LANGUAGES C) + +find_package( + Python + COMPONENTS Interpreter Development.Module + REQUIRED) + +python_add_library(emod MODULE emod.c WITH_SOABI) +install(TARGETS emod DESTINATION .) +install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/pmod.py" DESTINATION .) + +add_subdirectory(pkg) diff --git a/tests/packages/importlib_editable/emod.c b/tests/packages/importlib_editable/emod.c new file mode 100644 index 00000000..3e71f04e --- /dev/null +++ b/tests/packages/importlib_editable/emod.c @@ -0,0 +1,25 @@ +#define PY_SSIZE_T_CLEAN +#include + +float square(float x) { return x * x; } + +static PyObject *square_wrapper(PyObject *self, PyObject *args) { + float input, result; + if (!PyArg_ParseTuple(args, "f", &input)) { + return NULL; + } + result = square(input); + return PyFloat_FromDouble(result); +} + +static PyMethodDef emod_methods[] = { + {"square", square_wrapper, METH_VARARGS, "Square function"}, + {NULL, NULL, 0, NULL}}; + +static struct PyModuleDef emod_module = {PyModuleDef_HEAD_INIT, "emod", + NULL, -1, emod_methods}; + +/* name here must match extension name, with PyInit_ prefix */ +PyMODINIT_FUNC PyInit_emod(void) { + return PyModule_Create(&emod_module); +} diff --git a/tests/packages/importlib_editable/pkg/CMakeLists.txt b/tests/packages/importlib_editable/pkg/CMakeLists.txt new file mode 100644 index 00000000..9db38f05 --- /dev/null +++ b/tests/packages/importlib_editable/pkg/CMakeLists.txt @@ -0,0 +1,8 @@ +python_add_library(emod_a MODULE emod_a.c WITH_SOABI) + +install(TARGETS emod_a DESTINATION pkg/) +file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/testfile" "This is the file") +install(FILES "${CMAKE_CURRENT_BINARY_DIR}/testfile" DESTINATION pkg/) + +add_subdirectory(sub_a) +add_subdirectory(sub_b) diff --git a/tests/packages/importlib_editable/pkg/__init__.py b/tests/packages/importlib_editable/pkg/__init__.py new file mode 100644 index 00000000..906a2289 --- /dev/null +++ b/tests/packages/importlib_editable/pkg/__init__.py @@ -0,0 +1,48 @@ +# Don't let ruff sort imports in this file, we want to keep them with the comments as is +# for clarity. +# ruff: noqa: I001 + +# Level one pure modules +from . import pmod_a + +# Level one extension modules +from . import emod_a + +# Level one subpackages +from . import sub_a, sub_b + +# Level two pure modules +from .sub_a import pmod_b +from .sub_b import pmod_c + +# Level two extension modules +from .sub_a import emod_b +from .sub_b import emod_c + +# Level two subpackages +from .sub_b import sub_c, sub_d + +# Level three pure modules +from .sub_b.sub_c import pmod_d +from .sub_b.sub_d import pmod_e + +# Level three extension modules +from .sub_b.sub_c import emod_d +from .sub_b.sub_d import emod_e + +__all__ = [ + "emod_a", + "emod_b", + "emod_c", + "emod_d", + "emod_e", + "pmod_a", + "pmod_b", + "pmod_c", + "pmod_d", + "pmod_e", + "sub_a", + "sub_b", + "sub_c", + "sub_d", +] diff --git a/tests/packages/importlib_editable/pkg/emod_a.c b/tests/packages/importlib_editable/pkg/emod_a.c new file mode 100644 index 00000000..043116ca --- /dev/null +++ b/tests/packages/importlib_editable/pkg/emod_a.c @@ -0,0 +1,25 @@ +#define PY_SSIZE_T_CLEAN +#include + +float square(float x) { return x * x; } + +static PyObject *square_wrapper(PyObject *self, PyObject *args) { + float input, result; + if (!PyArg_ParseTuple(args, "f", &input)) { + return NULL; + } + result = square(input); + return PyFloat_FromDouble(result); +} + +static PyMethodDef emod_a_methods[] = { + {"square", square_wrapper, METH_VARARGS, "Square function"}, + {NULL, NULL, 0, NULL}}; + +static struct PyModuleDef emod_a_module = {PyModuleDef_HEAD_INIT, "emod_a", + NULL, -1, emod_a_methods}; + +/* name here must match extension name, with PyInit_ prefix */ +PyMODINIT_FUNC PyInit_emod_a(void) { + return PyModule_Create(&emod_a_module); +} diff --git a/tests/packages/importlib_editable/pkg/emod_a.pyi b/tests/packages/importlib_editable/pkg/emod_a.pyi new file mode 100644 index 00000000..e69de29b diff --git a/tests/packages/importlib_editable/pkg/pmod_a.py b/tests/packages/importlib_editable/pkg/pmod_a.py new file mode 100644 index 00000000..caf28fe9 --- /dev/null +++ b/tests/packages/importlib_editable/pkg/pmod_a.py @@ -0,0 +1,2 @@ +def square(x): + return x * x diff --git a/tests/packages/importlib_editable/pkg/sub_a/CMakeLists.txt b/tests/packages/importlib_editable/pkg/sub_a/CMakeLists.txt new file mode 100644 index 00000000..3e159b33 --- /dev/null +++ b/tests/packages/importlib_editable/pkg/sub_a/CMakeLists.txt @@ -0,0 +1,5 @@ +python_add_library(emod_b MODULE emod_b.c WITH_SOABI) + +install(TARGETS emod_b DESTINATION pkg/sub_a) +file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/testfile" "This is the file") +install(FILES "${CMAKE_CURRENT_BINARY_DIR}/testfile" DESTINATION pkg/sub_a) diff --git a/tests/packages/importlib_editable/pkg/sub_a/__init__.py b/tests/packages/importlib_editable/pkg/sub_a/__init__.py new file mode 100644 index 00000000..7eb1259b --- /dev/null +++ b/tests/packages/importlib_editable/pkg/sub_a/__init__.py @@ -0,0 +1,3 @@ +from . import emod_b, pmod_b + +__all__ = ["emod_b", "pmod_b"] diff --git a/tests/packages/importlib_editable/pkg/sub_a/emod_b.c b/tests/packages/importlib_editable/pkg/sub_a/emod_b.c new file mode 100644 index 00000000..51b14bb7 --- /dev/null +++ b/tests/packages/importlib_editable/pkg/sub_a/emod_b.c @@ -0,0 +1,25 @@ +#define PY_SSIZE_T_CLEAN +#include + +float square(float x) { return x * x; } + +static PyObject *square_wrapper(PyObject *self, PyObject *args) { + float input, result; + if (!PyArg_ParseTuple(args, "f", &input)) { + return NULL; + } + result = square(input); + return PyFloat_FromDouble(result); +} + +static PyMethodDef emod_b_methods[] = { + {"square", square_wrapper, METH_VARARGS, "Square function"}, + {NULL, NULL, 0, NULL}}; + +static struct PyModuleDef emod_b_module = {PyModuleDef_HEAD_INIT, "emod_b", + NULL, -1, emod_b_methods}; + +/* name here must match extension name, with PyInit_ prefix */ +PyMODINIT_FUNC PyInit_emod_b(void) { + return PyModule_Create(&emod_b_module); +} diff --git a/tests/packages/importlib_editable/pkg/sub_a/emod_b.pyi b/tests/packages/importlib_editable/pkg/sub_a/emod_b.pyi new file mode 100644 index 00000000..e69de29b diff --git a/tests/packages/importlib_editable/pkg/sub_a/pmod_b.py b/tests/packages/importlib_editable/pkg/sub_a/pmod_b.py new file mode 100644 index 00000000..caf28fe9 --- /dev/null +++ b/tests/packages/importlib_editable/pkg/sub_a/pmod_b.py @@ -0,0 +1,2 @@ +def square(x): + return x * x diff --git a/tests/packages/importlib_editable/pkg/sub_b/CMakeLists.txt b/tests/packages/importlib_editable/pkg/sub_b/CMakeLists.txt new file mode 100644 index 00000000..90af76b2 --- /dev/null +++ b/tests/packages/importlib_editable/pkg/sub_b/CMakeLists.txt @@ -0,0 +1,8 @@ +python_add_library(emod_c MODULE emod_c.c WITH_SOABI) + +install(TARGETS emod_c DESTINATION pkg/sub_b) +file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/testfile" "This is the file") +install(FILES "${CMAKE_CURRENT_BINARY_DIR}/testfile" DESTINATION pkg/sub_b) + +add_subdirectory(sub_c) +add_subdirectory(sub_d) diff --git a/tests/packages/importlib_editable/pkg/sub_b/__init__.py b/tests/packages/importlib_editable/pkg/sub_b/__init__.py new file mode 100644 index 00000000..907b1501 --- /dev/null +++ b/tests/packages/importlib_editable/pkg/sub_b/__init__.py @@ -0,0 +1,31 @@ +# Don't let ruff sort imports in this file, we want to keep them with the comments as is +# for clarity. +# ruff: noqa: I001 + +# Level one pure modules +from . import pmod_c + +# Level one extension modules +from . import emod_c + +# Level one subpackages +from . import sub_c, sub_d + +# Level two pure modules +from .sub_c import pmod_d +from .sub_d import pmod_e + +# Level two extension modules +from .sub_c import emod_d +from .sub_d import emod_e + +__all__ = [ + "emod_c", + "emod_d", + "emod_e", + "pmod_c", + "pmod_d", + "pmod_e", + "sub_c", + "sub_d", +] diff --git a/tests/packages/importlib_editable/pkg/sub_b/emod_c.c b/tests/packages/importlib_editable/pkg/sub_b/emod_c.c new file mode 100644 index 00000000..7cefd9d2 --- /dev/null +++ b/tests/packages/importlib_editable/pkg/sub_b/emod_c.c @@ -0,0 +1,25 @@ +#define PY_SSIZE_T_CLEAN +#include + +float square(float x) { return x * x; } + +static PyObject *square_wrapper(PyObject *self, PyObject *args) { + float input, result; + if (!PyArg_ParseTuple(args, "f", &input)) { + return NULL; + } + result = square(input); + return PyFloat_FromDouble(result); +} + +static PyMethodDef emod_c_methods[] = { + {"square", square_wrapper, METH_VARARGS, "Square function"}, + {NULL, NULL, 0, NULL}}; + +static struct PyModuleDef emod_c_module = {PyModuleDef_HEAD_INIT, "emod_c", + NULL, -1, emod_c_methods}; + +/* name here must match extension name, with PyInit_ prefix */ +PyMODINIT_FUNC PyInit_emod_c(void) { + return PyModule_Create(&emod_c_module); +} diff --git a/tests/packages/importlib_editable/pkg/sub_b/emod_c.pyi b/tests/packages/importlib_editable/pkg/sub_b/emod_c.pyi new file mode 100644 index 00000000..e69de29b diff --git a/tests/packages/importlib_editable/pkg/sub_b/pmod_c.py b/tests/packages/importlib_editable/pkg/sub_b/pmod_c.py new file mode 100644 index 00000000..caf28fe9 --- /dev/null +++ b/tests/packages/importlib_editable/pkg/sub_b/pmod_c.py @@ -0,0 +1,2 @@ +def square(x): + return x * x diff --git a/tests/packages/importlib_editable/pkg/sub_b/sub_c/CMakeLists.txt b/tests/packages/importlib_editable/pkg/sub_b/sub_c/CMakeLists.txt new file mode 100644 index 00000000..50bdbdd7 --- /dev/null +++ b/tests/packages/importlib_editable/pkg/sub_b/sub_c/CMakeLists.txt @@ -0,0 +1,6 @@ +python_add_library(emod_d MODULE emod_d.c WITH_SOABI) + +install(TARGETS emod_d DESTINATION pkg/sub_b/sub_c) +file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/testfile" "This is the file") +install(FILES "${CMAKE_CURRENT_BINARY_DIR}/testfile" + DESTINATION pkg/sub_b/sub_c/) diff --git a/tests/packages/importlib_editable/pkg/sub_b/sub_c/__init__.py b/tests/packages/importlib_editable/pkg/sub_b/sub_c/__init__.py new file mode 100644 index 00000000..d41c38b3 --- /dev/null +++ b/tests/packages/importlib_editable/pkg/sub_b/sub_c/__init__.py @@ -0,0 +1,3 @@ +from . import emod_d, pmod_d + +__all__ = ["emod_d", "pmod_d"] diff --git a/tests/packages/importlib_editable/pkg/sub_b/sub_c/emod_d.c b/tests/packages/importlib_editable/pkg/sub_b/sub_c/emod_d.c new file mode 100644 index 00000000..c1ca4f2e --- /dev/null +++ b/tests/packages/importlib_editable/pkg/sub_b/sub_c/emod_d.c @@ -0,0 +1,25 @@ +#define PY_SSIZE_T_CLEAN +#include + +float square(float x) { return x * x; } + +static PyObject *square_wrapper(PyObject *self, PyObject *args) { + float input, result; + if (!PyArg_ParseTuple(args, "f", &input)) { + return NULL; + } + result = square(input); + return PyFloat_FromDouble(result); +} + +static PyMethodDef emod_d_methods[] = { + {"square", square_wrapper, METH_VARARGS, "Square function"}, + {NULL, NULL, 0, NULL}}; + +static struct PyModuleDef emod_d_module = {PyModuleDef_HEAD_INIT, "emod_d", + NULL, -1, emod_d_methods}; + +/* name here must match extension name, with PyInit_ prefix */ +PyMODINIT_FUNC PyInit_emod_d(void) { + return PyModule_Create(&emod_d_module); +} diff --git a/tests/packages/importlib_editable/pkg/sub_b/sub_c/emod_d.pyi b/tests/packages/importlib_editable/pkg/sub_b/sub_c/emod_d.pyi new file mode 100644 index 00000000..e69de29b diff --git a/tests/packages/importlib_editable/pkg/sub_b/sub_c/pmod_d.py b/tests/packages/importlib_editable/pkg/sub_b/sub_c/pmod_d.py new file mode 100644 index 00000000..caf28fe9 --- /dev/null +++ b/tests/packages/importlib_editable/pkg/sub_b/sub_c/pmod_d.py @@ -0,0 +1,2 @@ +def square(x): + return x * x diff --git a/tests/packages/importlib_editable/pkg/sub_b/sub_d/CMakeLists.txt b/tests/packages/importlib_editable/pkg/sub_b/sub_d/CMakeLists.txt new file mode 100644 index 00000000..58af95ba --- /dev/null +++ b/tests/packages/importlib_editable/pkg/sub_b/sub_d/CMakeLists.txt @@ -0,0 +1,6 @@ +python_add_library(emod_e MODULE emod_e.c WITH_SOABI) + +install(TARGETS emod_e DESTINATION pkg/sub_b/sub_d/) +file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/testfile" "This is the file") +install(FILES "${CMAKE_CURRENT_BINARY_DIR}/testfile" + DESTINATION pkg/sub_b/sub_d/) diff --git a/tests/packages/importlib_editable/pkg/sub_b/sub_d/__init__.py b/tests/packages/importlib_editable/pkg/sub_b/sub_d/__init__.py new file mode 100644 index 00000000..3ca2841d --- /dev/null +++ b/tests/packages/importlib_editable/pkg/sub_b/sub_d/__init__.py @@ -0,0 +1,3 @@ +from . import emod_e, pmod_e + +__all__ = ["pmod_e", "emod_e"] diff --git a/tests/packages/importlib_editable/pkg/sub_b/sub_d/emod_e.c b/tests/packages/importlib_editable/pkg/sub_b/sub_d/emod_e.c new file mode 100644 index 00000000..878fd1c6 --- /dev/null +++ b/tests/packages/importlib_editable/pkg/sub_b/sub_d/emod_e.c @@ -0,0 +1,25 @@ +#define PY_SSIZE_T_CLEAN +#include + +float square(float x) { return x * x; } + +static PyObject *square_wrapper(PyObject *self, PyObject *args) { + float input, result; + if (!PyArg_ParseTuple(args, "f", &input)) { + return NULL; + } + result = square(input); + return PyFloat_FromDouble(result); +} + +static PyMethodDef emod_e_methods[] = { + {"square", square_wrapper, METH_VARARGS, "Square function"}, + {NULL, NULL, 0, NULL}}; + +static struct PyModuleDef emod_e_module = {PyModuleDef_HEAD_INIT, "emod_e", + NULL, -2, emod_e_methods}; + +/* name here must match extension name, with PyInit_ prefix */ +PyMODINIT_FUNC PyInit_emod_e(void) { + return PyModule_Create(&emod_e_module); +} diff --git a/tests/packages/importlib_editable/pkg/sub_b/sub_d/emod_e.pyi b/tests/packages/importlib_editable/pkg/sub_b/sub_d/emod_e.pyi new file mode 100644 index 00000000..e69de29b diff --git a/tests/packages/importlib_editable/pkg/sub_b/sub_d/pmod_e.py b/tests/packages/importlib_editable/pkg/sub_b/sub_d/pmod_e.py new file mode 100644 index 00000000..caf28fe9 --- /dev/null +++ b/tests/packages/importlib_editable/pkg/sub_b/sub_d/pmod_e.py @@ -0,0 +1,2 @@ +def square(x): + return x * x diff --git a/tests/packages/importlib_editable/pmod.py b/tests/packages/importlib_editable/pmod.py new file mode 100644 index 00000000..caf28fe9 --- /dev/null +++ b/tests/packages/importlib_editable/pmod.py @@ -0,0 +1,2 @@ +def square(x): + return x * x diff --git a/tests/packages/importlib_editable/pyproject.toml b/tests/packages/importlib_editable/pyproject.toml new file mode 100644 index 00000000..dae7eb5a --- /dev/null +++ b/tests/packages/importlib_editable/pyproject.toml @@ -0,0 +1,10 @@ +[build-system] +requires = ["scikit-build-core"] +build-backend = "scikit_build_core.build" + +[project] +name = "pkg" +version = "0.0.1" + +[tool.scikit-build] +build-dir = "build/{wheel_tag}" diff --git a/tests/test_editable.py b/tests/test_editable.py index 1d54d866..17ae8fa5 100644 --- a/tests/test_editable.py +++ b/tests/test_editable.py @@ -156,3 +156,174 @@ def test_install_dir(isolated): assert "Running cmake" in out assert c_module.exists() assert not failed_c_module.exists() + + +def _setup_package_for_editable_layout_tests( + monkeypatch, tmp_path, editable, editable_mode, isolated +): + editable_flag = ["-e"] if editable else [] + + config_mode_flags = [] + if editable: + config_mode_flags.append(f"--config-settings=editable.mode={editable_mode}") + if editable_mode != "inplace": + config_mode_flags.append("--config-settings=build-dir=build/{wheel_tag}") + + # Use a context so that we only change into the directory up until the point where + # we run the editable install. We do not want to be in that directory when importing + # to avoid importing the source directory instead of the installed package. + with monkeypatch.context() as m: + package = PackageInfo("importlib_editable") + process_package(package, tmp_path, m) + + ninja = [ + "ninja" + for f in isolated.wheelhouse.iterdir() + if f.name.startswith("ninja-") + ] + cmake = [ + "cmake" + for f in isolated.wheelhouse.iterdir() + if f.name.startswith("cmake-") + ] + + isolated.install("pip>23") + isolated.install("scikit-build-core", *ninja, *cmake) + + isolated.install( + "-v", + *config_mode_flags, + "--no-build-isolation", + *editable_flag, + ".", + ) + + +@pytest.mark.compile +@pytest.mark.configure +@pytest.mark.integration +@pytest.mark.parametrize( + ("editable", "editable_mode"), [(False, ""), (True, "redirect"), (True, "inplace")] +) +def test_direct_import(monkeypatch, tmp_path, editable, editable_mode, isolated): + _setup_package_for_editable_layout_tests( # type: ignore[no-untyped-call] + monkeypatch, tmp_path, editable, editable_mode, isolated + ) + isolated.execute("import pkg") + + +@pytest.mark.compile +@pytest.mark.configure +@pytest.mark.integration +@pytest.mark.parametrize( + ("editable", "editable_mode", "check"), + [ + # Without editable + (False, "", "isinstance(files(pkg), pathlib.Path)"), + (False, "", "any(str(x).endswith('.so') for x in files(pkg).iterdir())"), + (False, "", "isinstance(files(pkg.sub_a), pathlib.Path)"), + ( + False, + "", + "any(str(x).endswith('.so') for x in files(pkg.sub_a).iterdir())", + ), + (False, "", "isinstance(files(pkg.sub_b), pathlib.Path)"), + ( + False, + "", + "any(str(x).endswith('.so') for x in files(pkg.sub_b).iterdir())", + ), + (False, "", "isinstance(files(pkg.sub_b.sub_c), pathlib.Path)"), + ( + False, + "", + "any(str(x).endswith('.so') for x in files(pkg.sub_b.sub_c).iterdir())", + ), + (False, "", "isinstance(files(pkg.sub_b.sub_d), pathlib.Path)"), + ( + False, + "", + "any(str(x).endswith('.so') for x in files(pkg.sub_b.sub_d).iterdir())", + ), + # Editable redirect + (True, "redirect", "isinstance(files(pkg), pathlib.Path)"), + pytest.param( + True, + "redirect", + "any(str(x).endswith('.so') for x in files(pkg).iterdir())", + marks=pytest.mark.xfail, + ), + (True, "redirect", "isinstance(files(pkg.sub_a), pathlib.Path)"), + pytest.param( + True, + "redirect", + "any(str(x).endswith('.so') for x in files(pkg.sub_a).iterdir())", + marks=pytest.mark.xfail, + ), + (True, "redirect", "isinstance(files(pkg.sub_b), pathlib.Path)"), + pytest.param( + True, + "redirect", + "any(str(x).endswith('.so') for x in files(pkg.sub_b).iterdir())", + marks=pytest.mark.xfail, + ), + (True, "redirect", "isinstance(files(pkg.sub_b.sub_c), pathlib.Path)"), + pytest.param( + True, + "redirect", + "any(str(x).endswith('.so') for x in files(pkg.sub_b.sub_c).iterdir())", + marks=pytest.mark.xfail, + ), + (True, "redirect", "isinstance(files(pkg.sub_b.sub_d), pathlib.Path)"), + pytest.param( + True, + "redirect", + "any(str(x).endswith('.so') for x in files(pkg.sub_b.sub_d).iterdir())", + marks=pytest.mark.xfail, + ), + # Editable inplace + (True, "inplace", "isinstance(files(pkg), pathlib.Path)"), + (True, "inplace", "any(str(x).endswith('.so') for x in files(pkg).iterdir())"), + (True, "inplace", "isinstance(files(pkg.sub_a), pathlib.Path)"), + ( + True, + "inplace", + "any(str(x).endswith('.so') for x in files(pkg.sub_a).iterdir())", + ), + (True, "inplace", "isinstance(files(pkg.sub_b), pathlib.Path)"), + ( + True, + "inplace", + "any(str(x).endswith('.so') for x in files(pkg.sub_b).iterdir())", + ), + (True, "inplace", "isinstance(files(pkg.sub_b.sub_c), pathlib.Path)"), + ( + True, + "inplace", + "any(str(x).endswith('.so') for x in files(pkg.sub_b.sub_c).iterdir())", + ), + (True, "inplace", "isinstance(files(pkg.sub_b.sub_d), pathlib.Path)"), + ( + True, + "inplace", + "any(str(x).endswith('.so') for x in files(pkg.sub_b.sub_d).iterdir())", + ), + ], +) +def test_importlib_resources( + monkeypatch, tmp_path, editable, editable_mode, isolated, check +): + _setup_package_for_editable_layout_tests( # type: ignore[no-untyped-call] + monkeypatch, tmp_path, editable, editable_mode, isolated + ) + isolated.execute( + textwrap.dedent( + f""" + from importlib.resources import files + from importlib.readers import MultiplexedPath + import pkg + import pathlib + assert {check} + """ + ) + )