diff --git a/tests/packages/cmake_generated/CMakeLists.txt b/tests/packages/cmake_generated/CMakeLists.txt new file mode 100644 index 00000000..6fa55eec --- /dev/null +++ b/tests/packages/cmake_generated/CMakeLists.txt @@ -0,0 +1,49 @@ +cmake_minimum_required(VERSION 3.15) + +project( + ${SKBUILD_PROJECT_NAME} + LANGUAGES CXX + VERSION 1.2.3) + +# Generate files at config time (configure_file) and at build time +# (add_custom_command) Note that bundling a generated file with sdist is out of +# scope for now. Note: cmake_generated/nested1/generated.py should try to open +# both generated and static files. +configure_file(src/cmake_generated/nested1/generated.py.in generated.py) +# We always expect the install phase to run, so the build tree layout can be +# different than the package layout. +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/generated.py + DESTINATION ${SKBUILD_PROJECT_NAME}/nested1) + +file( + GENERATE + OUTPUT configured_file + CONTENT "value written by cmake file generation") +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/configured_file + DESTINATION ${SKBUILD_PROJECT_NAME}) + +set(OUTPUT_FILE "${CMAKE_CURRENT_BINARY_DIR}/generated_data") +set(FILE_CONTENT "value written by cmake custom_command") +set(GENERATE_SCRIPT "file(WRITE \"${OUTPUT_FILE}\" \"${FILE_CONTENT}\")") +add_custom_command( + OUTPUT "${OUTPUT_FILE}" + COMMAND "${CMAKE_COMMAND}" -P + "${CMAKE_CURRENT_BINARY_DIR}/generate_file.cmake" + DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/generate_file.cmake" + COMMENT "Generating ${OUTPUT_FILE} using CMake scripting at build time" + VERBATIM) +file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/generate_file.cmake" + "${GENERATE_SCRIPT}") +add_custom_target(generate_file ALL DEPENDS "${OUTPUT_FILE}") +install(FILES "${OUTPUT_FILE}" DESTINATION ${SKBUILD_PROJECT_NAME}/namespace1) + +add_library(pkg MODULE src/cmake_generated/pkg.cpp) + +if(NOT WIN32) + # Explicitly set the bundle extension to .so + set_target_properties(pkg PROPERTIES SUFFIX ".so") +endif() + +# Set the library name to "pkg", regardless of the OS convention. +set_target_properties(pkg PROPERTIES PREFIX "") +install(TARGETS pkg DESTINATION ${SKBUILD_PROJECT_NAME}) diff --git a/tests/packages/cmake_generated/pyproject.toml b/tests/packages/cmake_generated/pyproject.toml new file mode 100644 index 00000000..da2c8081 --- /dev/null +++ b/tests/packages/cmake_generated/pyproject.toml @@ -0,0 +1,24 @@ +[build-system] +requires = ["scikit-build-core"] +build-backend = "scikit_build_core.build" + +[project] +name = "cmake_generated" +dynamic = ["version"] + +[tool.scikit-build] +# Bundling a generated file in the sdist is not supported at this time. +# sdist.cmake = false +wheel.license-files = [] +wheel.exclude = ["**.cpp", "**.in"] + +[tool.scikit-build.metadata.version] +provider = "scikit_build_core.metadata.regex" +input = "CMakeLists.txt" +regex = 'project\([^)]+ VERSION (?P[0-9.]+)' + +[[tool.scikit-build.generate]] +path = "cmake_generated/_version.py" +template = ''' +__version__ = "${version}" +''' diff --git a/tests/packages/cmake_generated/src/cmake_generated/__init__.py b/tests/packages/cmake_generated/src/cmake_generated/__init__.py new file mode 100644 index 00000000..80081d78 --- /dev/null +++ b/tests/packages/cmake_generated/src/cmake_generated/__init__.py @@ -0,0 +1,67 @@ +"""Package that includes several non-Python non-module files. + +Support some test cases aimed at testing our ability to find generated files +and static package data files in editable installations. + +We are exercising the importlib machinery to find files that +are generated in different phases of the build and in different parts of +the package layout to check that the redirection works correctly in an +editable installation. + +The test package includes raw data files and shared object libraries that +are accessed via `ctypes`. + +We test files (generated and static) + +* at the top level of the package, +* in subpackages, and +* in a namespace package. + +We test access + +* from modules at the same level as the files, +* one level above and below, and +* from parallel subpackages. + +Question: Do we want to test both relative and absolute imports or just one or the other? +""" + +import ctypes +import sys +from importlib.resources import as_file, files, read_text + +try: + from ._version import __version__ +except ImportError: + __version__ = None + + +def get_static_data(): + return read_text("cmake_generated", "static_data").rstrip() + + +def get_configured_data(): + return files().joinpath("configured_file").read_text().rstrip() + + +def get_namespace_static_data(): + # read_text is able to handle a namespace subpackage directly, though `files()` is not. + return read_text("cmake_generated.namespace1", "static_data").rstrip() + + +def get_namespace_generated_data(): + # Note that `files("cmake_generated.namespace1")` doesn't work. + # Ref https://github.com/python/importlib_resources/issues/262 + return ( + files().joinpath("namespace1").joinpath("generated_data").read_text().rstrip() + ) + + +def ctypes_function(): + if sys.platform == "win32": + lib_suffix = "dll" + else: + lib_suffix = "so" + with as_file(files().joinpath(f"pkg.{lib_suffix}")) as lib_path: + lib = ctypes.cdll.LoadLibrary(str(lib_path)) + return lib.func diff --git a/tests/packages/cmake_generated/src/cmake_generated/namespace1/static_data b/tests/packages/cmake_generated/src/cmake_generated/namespace1/static_data new file mode 100644 index 00000000..2e114a4b --- /dev/null +++ b/tests/packages/cmake_generated/src/cmake_generated/namespace1/static_data @@ -0,0 +1 @@ +static value in namespace package diff --git a/tests/packages/cmake_generated/src/cmake_generated/nested1/__init__.py b/tests/packages/cmake_generated/src/cmake_generated/nested1/__init__.py new file mode 100644 index 00000000..71741dd5 --- /dev/null +++ b/tests/packages/cmake_generated/src/cmake_generated/nested1/__init__.py @@ -0,0 +1,5 @@ +from importlib.resources import read_text + + +def get_static_data(): + return read_text("cmake_generated.nested1", "static_data").rstrip() diff --git a/tests/packages/cmake_generated/src/cmake_generated/nested1/generated.py.in b/tests/packages/cmake_generated/src/cmake_generated/nested1/generated.py.in new file mode 100644 index 00000000..f39f510b --- /dev/null +++ b/tests/packages/cmake_generated/src/cmake_generated/nested1/generated.py.in @@ -0,0 +1,27 @@ +"""Try to open both generated and static files from various parts of the package.""" +from importlib.resources import files, read_text + +from .. import __version__ + +try: + from .. import nested2 +except ImportError: + nested2 = None + +def cmake_generated_static_data(): + return read_text("cmake_generated", "static_data").rstrip() + +def cmake_generated_nested_static_data(): + return files("cmake_generated.nested1").joinpath("static_data").read_text().rstrip() + +def get_configured_data(): + return files("cmake_generated").joinpath("configured_file").read_text().rstrip() + +def cmake_generated_namespace_generated_data(): + return read_text("cmake_generated.namespace1","generated_data").rstrip() + +nested_data = "success" + +def nested2_check(): + if nested2 is not None: + return "success" diff --git a/tests/packages/cmake_generated/src/cmake_generated/nested1/static_data b/tests/packages/cmake_generated/src/cmake_generated/nested1/static_data new file mode 100644 index 00000000..bdc4abc1 --- /dev/null +++ b/tests/packages/cmake_generated/src/cmake_generated/nested1/static_data @@ -0,0 +1 @@ +static value in subpackage 1 diff --git a/tests/packages/cmake_generated/src/cmake_generated/nested2/__init__.py b/tests/packages/cmake_generated/src/cmake_generated/nested2/__init__.py new file mode 100644 index 00000000..ac9993e7 --- /dev/null +++ b/tests/packages/cmake_generated/src/cmake_generated/nested2/__init__.py @@ -0,0 +1,5 @@ +def nested1_generated_check(): + # noinspection PyUnresolvedReferences + from ..nested1.generated import nested_data + + return nested_data diff --git a/tests/packages/cmake_generated/src/cmake_generated/pkg.cpp b/tests/packages/cmake_generated/src/cmake_generated/pkg.cpp new file mode 100644 index 00000000..9572bf14 --- /dev/null +++ b/tests/packages/cmake_generated/src/cmake_generated/pkg.cpp @@ -0,0 +1 @@ +extern "C" int func() {return 42;} diff --git a/tests/packages/cmake_generated/src/cmake_generated/static_data b/tests/packages/cmake_generated/src/cmake_generated/static_data new file mode 100644 index 00000000..6d040033 --- /dev/null +++ b/tests/packages/cmake_generated/src/cmake_generated/static_data @@ -0,0 +1 @@ +static value in top-level package diff --git a/tests/test_editable_generated.py b/tests/test_editable_generated.py new file mode 100644 index 00000000..5825d1ef --- /dev/null +++ b/tests/test_editable_generated.py @@ -0,0 +1,301 @@ +"""Test regular and editable installs with generated files. + +Illustrate the supported and correct ways to use generated files +(other than traditional compiled extension modules). + +Check a variety of scenarios in which package files (modules or data) are +not present in the source tree to confirm that we can find resources as expected, +either by ``import`` or with tools such as `importlib.resources.files()`. +""" + +from __future__ import annotations + +import sys +from pathlib import Path + +import pytest +from conftest import PackageInfo, VEnv, process_package + + +def _setup_package_for_editable_layout_tests( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, + editable: bool, + editable_mode: str, + build_isolation: bool, + isolated: VEnv, +) -> None: + 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("cmake_generated") + process_package(package, tmp_path, m) + + assert isolated.wheelhouse + + 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") + if not build_isolation: + isolated.install("scikit-build-core", *ninja, *cmake) + + isolated.install( + "-v", + *config_mode_flags, + *editable_flag, + ".", + isolated=build_isolation, + ) + + +@pytest.mark.compile +@pytest.mark.configure +@pytest.mark.integration +@pytest.mark.parametrize( + ("editable", "editable_mode"), + [ + (False, ""), + (True, "redirect"), + (True, "inplace"), + ], +) +@pytest.mark.parametrize( + "build_isolation", + [True, False], +) +@pytest.mark.skipif( + sys.version_info < (3, 9), + reason="importlib.resources.files is introduced in Python 3.9", +) +def test_basic_data_resources( + monkeypatch, tmp_path, editable, editable_mode, build_isolation, isolated +): + _setup_package_for_editable_layout_tests( + monkeypatch, tmp_path, editable, editable_mode, build_isolation, isolated + ) + + value = isolated.execute( + "import cmake_generated; print(cmake_generated.get_static_data())" + ) + assert value == "static value in top-level package" + + value = isolated.execute( + "import cmake_generated.nested1; print(cmake_generated.nested1.get_static_data())" + ) + assert value == "static value in subpackage 1" + + value = isolated.execute( + "import cmake_generated; print(cmake_generated.get_namespace_static_data())" + ) + assert value == "static value in namespace package" + + +@pytest.mark.compile +@pytest.mark.configure +@pytest.mark.integration +@pytest.mark.parametrize( + ("editable", "editable_mode"), + [ + (False, ""), + (True, "redirect"), + (True, "inplace"), + ], +) +@pytest.mark.parametrize( + "build_isolation", + [True, False], +) +@pytest.mark.skipif( + sys.version_info < (3, 9), + reason="importlib.resources.files is introduced in Python 3.9", +) +def test_configure_time_generated_data( + monkeypatch, tmp_path, editable, editable_mode, build_isolation, isolated +): + _setup_package_for_editable_layout_tests( + monkeypatch, tmp_path, editable, editable_mode, build_isolation, isolated + ) + + value = isolated.execute( + "import cmake_generated; print(cmake_generated.get_configured_data())" + ) + assert value == "value written by cmake file generation" + + +@pytest.mark.compile +@pytest.mark.configure +@pytest.mark.integration +@pytest.mark.parametrize( + ("editable", "editable_mode"), + [ + (False, ""), + (True, "redirect"), + (True, "inplace"), + ], +) +@pytest.mark.parametrize( + "build_isolation", + [True, False], +) +@pytest.mark.skipif( + sys.version_info < (3, 9), + reason="importlib.resources.files is introduced in Python 3.9", +) +def test_build_time_generated_data( + monkeypatch, tmp_path, editable, editable_mode, build_isolation, isolated +): + _setup_package_for_editable_layout_tests( + monkeypatch, tmp_path, editable, editable_mode, build_isolation, isolated + ) + + value = isolated.execute( + "import cmake_generated; print(cmake_generated.get_namespace_generated_data())" + ) + assert value == "value written by cmake custom_command" + + +@pytest.mark.compile +@pytest.mark.configure +@pytest.mark.integration +@pytest.mark.parametrize( + ("editable", "editable_mode"), + [ + (False, ""), + (True, "redirect"), + (True, "inplace"), + ], +) +@pytest.mark.parametrize( + "build_isolation", + [True, False], +) +@pytest.mark.skipif( + sys.version_info < (3, 9), + reason="importlib.resources.files is introduced in Python 3.9", +) +def test_compiled_ctypes_resource( + monkeypatch, tmp_path, editable, editable_mode, build_isolation, isolated +): + _setup_package_for_editable_layout_tests( + monkeypatch, tmp_path, editable, editable_mode, build_isolation, isolated + ) + + value = isolated.execute( + "import cmake_generated; print(cmake_generated.ctypes_function()())" + ) + assert value == str(42) + + +@pytest.mark.compile +@pytest.mark.configure +@pytest.mark.integration +@pytest.mark.parametrize( + ("editable", "editable_mode"), + [ + (False, ""), + (True, "redirect"), + (True, "inplace"), + ], +) +@pytest.mark.parametrize( + "build_isolation", + [True, False], +) +@pytest.mark.skipif( + sys.version_info < (3, 9), + reason="importlib.resources.files is introduced in Python 3.9", +) +def test_configure_time_generated_module( + monkeypatch, tmp_path, editable, editable_mode, build_isolation, isolated +): + _setup_package_for_editable_layout_tests( + monkeypatch, tmp_path, editable, editable_mode, build_isolation, isolated + ) + # Check that a generated module can access and be accessed by all parts of the package + + value = isolated.execute( + "from cmake_generated.nested1.generated import __version__; print(__version__)" + ) + assert value == "1.2.3" + + value = isolated.execute( + "from cmake_generated.nested1.generated import cmake_generated_static_data; print(cmake_generated_static_data())" + ) + assert value == "static value in top-level package" + + value = isolated.execute( + "from cmake_generated.nested1.generated import cmake_generated_nested_static_data; print(cmake_generated_nested_static_data())" + ) + assert value == "static value in subpackage 1" + + value = isolated.execute( + "from cmake_generated.nested1.generated import cmake_generated_namespace_generated_data; print(cmake_generated_namespace_generated_data())" + ) + assert value == "value written by cmake custom_command" + + value = isolated.execute( + "from cmake_generated.nested1.generated import nested_data; print(nested_data)" + ) + assert value == "success" + value = isolated.execute( + "from cmake_generated.nested1.generated import nested2_check; print(nested2_check())" + ) + assert value == "success" + value = isolated.execute( + "from cmake_generated.nested2 import nested1_generated_check; print(nested1_generated_check())" + ) + assert value == "success" + + +@pytest.mark.compile +@pytest.mark.configure +@pytest.mark.integration +@pytest.mark.parametrize( + ("editable", "editable_mode"), + [ + (False, ""), + (True, "redirect"), + (True, "inplace"), + ], +) +@pytest.mark.parametrize( + "build_isolation", + [True, False], +) +@pytest.mark.skipif( + sys.version_info < (3, 9), + reason="importlib.resources.files is introduced in Python 3.9", +) +def test_build_time_generated_module( + monkeypatch, tmp_path, editable, editable_mode, build_isolation, isolated +): + _setup_package_for_editable_layout_tests( + monkeypatch, tmp_path, editable, editable_mode, build_isolation, isolated + ) + # Check generated _version module + attr_value = isolated.execute( + "import cmake_generated; print(cmake_generated.__version__)" + ) + assert attr_value == "1.2.3" + metadata_value = isolated.execute( + "import importlib.metadata; print(importlib.metadata.version('cmake_generated'))" + ) + assert metadata_value == "1.2.3"