Skip to content

Commit cabd073

Browse files
tests: cases for different use cases of generated files (#1193)
Per conference calls, these are some test cases relevant to #808 and #1160. Use cases for which support is targeted are marked `xfail` for now. We illustrate that, since `inplace` mode uses the build tree, we do not expect `inplace` editable mode to work with CMake projects whose build tree layout does not match the source tree layout. In conference calls, we decided that the only first class support would probably be for cases where the CMake install phase actually happens. This support decision warrants clarification, but is deferred to issues like #808. For now (for clarity), the unsupported cases are still in the parameterization matrix, but are annotated with `pytest.mark.skip`. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 51a6bb0 commit cabd073

12 files changed

Lines changed: 557 additions & 0 deletions

File tree

tests/conftest.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,10 @@ def prepare_no_build_isolation(self) -> None:
160160

161161
@pytest.fixture
162162
def isolated(tmp_path: Path, pep518_wheelhouse: Path) -> VEnv:
163+
"""Isolated virtual environment.
164+
165+
To control build isolation, see :py:func:`isolate`
166+
"""
163167
path = tmp_path / "venv"
164168
return VEnv(path, wheelhouse=pep518_wheelhouse)
165169

@@ -212,6 +216,13 @@ def package(
212216
tmp_path_factory: pytest.TempPathFactory,
213217
monkeypatch: pytest.MonkeyPatch,
214218
) -> PackageInfo:
219+
"""Get the test package.
220+
221+
Parameterize this fixture with the package name in the tests/packages directory.
222+
(Use ``indirect=True`` to pass the parameterization value to the fixture instead of
223+
directly to the test function.)
224+
https://docs.pytest.org/en/stable/example/parametrize.html#indirect-parametrization
225+
"""
215226
pkg_name = request.param
216227
assert isinstance(pkg_name, str)
217228
package = PackageInfo(pkg_name, tmp_path_factory.mktemp("pkg"))
@@ -256,12 +267,18 @@ def package_simple_pyproject_ext(
256267

257268
@dataclasses.dataclass(frozen=True)
258269
class Isolate:
270+
"""Selection for build isolation."""
271+
259272
state: bool
260273
flags: list[str]
261274

262275

263276
@pytest.fixture(params=[True, False], ids=["isolated", "not_isolated"])
264277
def isolate(request: pytest.FixtureRequest, isolated: VEnv) -> Isolate:
278+
"""Control build isolation.
279+
280+
For an isolated virtual environment, see :py:func:`isolated`
281+
"""
265282
isolate_request = request.param
266283
assert isinstance(isolate_request, bool)
267284
if not isolate_request:
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
cmake_minimum_required(VERSION 3.15)
2+
3+
project(
4+
${SKBUILD_PROJECT_NAME}
5+
LANGUAGES CXX
6+
VERSION 1.2.3)
7+
8+
# Generate files at config time (configure_file) and at build time
9+
# (add_custom_command) Note that bundling a generated file with sdist is out of
10+
# scope for now. Note: cmake_generated/nested1/generated.py should try to open
11+
# both generated and static files.
12+
configure_file(src/cmake_generated/nested1/generated.py.in generated.py)
13+
# We always expect the install phase to run, so the build tree layout can be
14+
# different than the package layout.
15+
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/generated.py
16+
DESTINATION ${SKBUILD_PROJECT_NAME}/nested1)
17+
18+
file(
19+
GENERATE
20+
OUTPUT configured_file
21+
CONTENT "value written by cmake file generation")
22+
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/configured_file
23+
DESTINATION ${SKBUILD_PROJECT_NAME})
24+
25+
set(OUTPUT_FILE "${CMAKE_CURRENT_BINARY_DIR}/generated_data")
26+
set(FILE_CONTENT "value written by cmake custom_command")
27+
set(GENERATE_SCRIPT "file(WRITE \"${OUTPUT_FILE}\" \"${FILE_CONTENT}\")")
28+
add_custom_command(
29+
OUTPUT "${OUTPUT_FILE}"
30+
COMMAND "${CMAKE_COMMAND}" -P
31+
"${CMAKE_CURRENT_BINARY_DIR}/generate_file.cmake"
32+
DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/generate_file.cmake"
33+
COMMENT "Generating ${OUTPUT_FILE} using CMake scripting at build time"
34+
VERBATIM)
35+
file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/generate_file.cmake"
36+
"${GENERATE_SCRIPT}")
37+
add_custom_target(generate_file ALL DEPENDS "${OUTPUT_FILE}")
38+
install(FILES "${OUTPUT_FILE}" DESTINATION ${SKBUILD_PROJECT_NAME}/namespace1)
39+
40+
add_library(pkg MODULE src/cmake_generated/pkg.cpp)
41+
include(GenerateExportHeader)
42+
generate_export_header(pkg)
43+
target_include_directories(pkg PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
44+
45+
if(NOT WIN32)
46+
# Explicitly set the bundle extension to .so
47+
set_target_properties(pkg PROPERTIES SUFFIX ".so")
48+
endif()
49+
50+
# Set the library name to "pkg", regardless of the OS convention.
51+
set_target_properties(pkg PROPERTIES PREFIX "")
52+
install(TARGETS pkg DESTINATION ${SKBUILD_PROJECT_NAME})
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
[build-system]
2+
requires = ["scikit-build-core"]
3+
build-backend = "scikit_build_core.build"
4+
5+
[project]
6+
name = "cmake_generated"
7+
dynamic = ["version"]
8+
9+
[tool.scikit-build]
10+
# Bundling a generated file in the sdist is not supported at this time.
11+
# sdist.cmake = false
12+
wheel.license-files = []
13+
wheel.exclude = ["**.cpp", "**.in"]
14+
15+
[tool.scikit-build.metadata.version]
16+
provider = "scikit_build_core.metadata.regex"
17+
input = "CMakeLists.txt"
18+
regex = 'project\([^)]+ VERSION (?P<value>[0-9.]+)'
19+
20+
[[tool.scikit-build.generate]]
21+
path = "cmake_generated/_version.py"
22+
template = '''
23+
__version__ = "${version}"
24+
'''
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
"""Package that includes several non-Python non-module files.
2+
3+
Support some test cases aimed at testing our ability to find generated files
4+
and static package data files in editable installations.
5+
6+
We are exercising the importlib machinery to find files that
7+
are generated in different phases of the build and in different parts of
8+
the package layout to check that the redirection works correctly in an
9+
editable installation.
10+
11+
The test package includes raw data files and shared object libraries that
12+
are accessed via `ctypes`.
13+
14+
We test files (generated and static)
15+
16+
* at the top level of the package,
17+
* in subpackages, and
18+
* in a namespace package.
19+
20+
We test access
21+
22+
* from modules at the same level as the files,
23+
* one level above and below, and
24+
* from parallel subpackages.
25+
"""
26+
27+
import ctypes
28+
import sys
29+
from importlib.resources import as_file, files, read_text
30+
31+
try:
32+
from ._version import __version__ # type: ignore[import-not-found]
33+
except ImportError:
34+
__version__ = None
35+
36+
37+
def get_static_data():
38+
return read_text("cmake_generated", "static_data").rstrip()
39+
40+
41+
def get_configured_data():
42+
return files("cmake_generated").joinpath("configured_file").read_text().rstrip()
43+
44+
45+
def get_namespace_static_data():
46+
if sys.version_info[0:2] == (3, 9):
47+
return (
48+
files("cmake_generated")
49+
.joinpath("namespace1/static_data")
50+
.read_text()
51+
.rstrip()
52+
)
53+
# (except in Python 3.9) read_text is able to handle a namespace subpackage directly, though `files()` is not.
54+
return read_text("cmake_generated.namespace1", "static_data").rstrip()
55+
56+
57+
def get_namespace_generated_data():
58+
# Note that `files("cmake_generated.namespace1")` doesn't work.
59+
# Ref https://github.com/python/importlib_resources/issues/262
60+
return (
61+
files("cmake_generated")
62+
.joinpath("namespace1/generated_data")
63+
.read_text()
64+
.rstrip()
65+
)
66+
67+
68+
def ctypes_function():
69+
if sys.platform == "win32":
70+
lib_suffix = "dll"
71+
else:
72+
lib_suffix = "so"
73+
with as_file(files("cmake_generated").joinpath(f"pkg.{lib_suffix}")) as lib_path:
74+
lib = ctypes.cdll.LoadLibrary(str(lib_path))
75+
return lib.func
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
static value in namespace package
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from importlib.resources import read_text
2+
3+
4+
def get_static_data():
5+
return read_text("cmake_generated.nested1", "static_data").rstrip()
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
"""Try to open both generated and static files from various parts of the package."""
2+
import sys
3+
from importlib.resources import files, read_text
4+
from types import ModuleType
5+
6+
from .. import __version__
7+
8+
try:
9+
from .. import nested2
10+
except ImportError as e:
11+
nested2 = None
12+
import_error = e.msg
13+
else:
14+
import_error = None
15+
16+
def cmake_generated_static_data():
17+
return read_text("cmake_generated", "static_data").rstrip()
18+
19+
def cmake_generated_nested_static_data():
20+
return files("cmake_generated.nested1").joinpath("static_data").read_text().rstrip()
21+
22+
def cmake_generated_namespace_static_data():
23+
# Note that `files("cmake_generated.namespace1")` doesn't work.
24+
# Ref https://github.com/python/importlib_resources/issues/262
25+
return files("cmake_generated").joinpath("namespace1/static_data").read_text().rstrip()
26+
27+
def get_configured_data():
28+
return files("cmake_generated").joinpath("configured_file").read_text().rstrip()
29+
30+
def cmake_generated_namespace_generated_data():
31+
if sys.version_info[0:2] == (3, 9):
32+
return files("cmake_generated").joinpath("namespace1/generated_data").read_text().rstrip()
33+
else:
34+
# (except in Python 3.9) read_text is able to handle a namespace subpackage directly, though `files()` is not.
35+
return read_text("cmake_generated.namespace1","generated_data").rstrip()
36+
37+
nested_data = "success"
38+
39+
def nested2_check():
40+
if import_error is not None:
41+
return import_error
42+
assert isinstance(nested2, ModuleType)
43+
return "success"
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
static value in subpackage 1
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
def nested1_generated_check():
2+
# noinspection PyUnresolvedReferences
3+
from ..nested1.generated import nested_data # type: ignore[import-not-found]
4+
5+
return nested_data
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#include "pkg_export.h"
2+
3+
extern "C" PKG_EXPORT int func() {return 42;}

0 commit comments

Comments
 (0)