Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support getting files in the build tree from editable installs #808

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions src/scikit_build_core/resources/_editable_redirect.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from __future__ import annotations

import importlib.abc
import importlib.machinery
import importlib.readers # Might be Python version specific?
import importlib.util
import os
import subprocess
Expand All @@ -22,6 +24,27 @@ def __dir__() -> list[str]:
return __all__


# Note: This solution relies on importlib's call stack in Python 3.11. Python 3.9 looks
# different, so might require a different solution, but I haven't gone deeper into that
# yet since I don't have a solution for the 3.11 case yet anyway.
class ScikitBuildRedirectingReader(importlib.readers.FileReader):
def files(self):
# ATTENTION: This is where the problem is. The expectation is that this returns
# a Traversable object. We could hack together an object that satisfies that
# API, but methods like `joinpath` don't have sensible implementations if
# `files` could return multiple paths instead of a single one. We could do some
# hackery to figure out which paths exist on the backend by hiding some internal
# representation that knows both possible roots and checks for existence when
# necessary, but that seriously violates the principle of least surprise for the
# user so I'd be quite skeptical.
return self.path


class ScikitBuildRedirectingLoader(importlib.machinery.SourceFileLoader):
def get_resource_reader(self, module):
return ScikitBuildRedirectingReader(self)


class ScikitBuildRedirectingFinder(importlib.abc.MetaPathFinder):
def __init__(
self,
Expand Down Expand Up @@ -92,13 +115,17 @@ def find_spec(
fullname,
os.path.join(self.dir, redir),
submodule_search_locations=submodule_search_locations,
loader=ScikitBuildRedirectingLoader(
fullname, os.path.join(self.dir, redir)
),
)
if fullname in self.known_source_files:
redir = self.known_source_files[fullname]
return importlib.util.spec_from_file_location(
fullname,
redir,
submodule_search_locations=submodule_search_locations,
loader=ScikitBuildRedirectingLoader(fullname, redir),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fact that this is specifying a single directory (the redir) is perhaps part of where we could improve things. (Sub)packages are found in the known source files list by their __init__.py. As a result, that path is used as the root, which means that we cannot find files nested in the package but present in the build directory because the package does not exist in known_wheel_files.

)
return None

Expand Down
13 changes: 13 additions & 0 deletions tests/packages/importlib_editable/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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(example MODULE example/example.c WITH_SOABI)

install(TARGETS example DESTINATION example/)
file(WRITE "${CMAKE_BINARY_DIR}/testfile" "This is the file")
install(FILES "${CMAKE_BINARY_DIR}/testfile" DESTINATION example/)
Empty file.
25 changes: 25 additions & 0 deletions tests/packages/importlib_editable/example/example.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#define PY_SSIZE_T_CLEAN
#include <Python.h>

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 example_methods[] = {
{"square", square_wrapper, METH_VARARGS, "Square function"},
{NULL, NULL, 0, NULL}};

static struct PyModuleDef example_module = {PyModuleDef_HEAD_INIT, "example",
NULL, -1, example_methods};

/* name here must match extension name, with PyInit_ prefix */
PyMODINIT_FUNC PyInit_example(void) {
return PyModule_Create(&example_module);
}
10 changes: 10 additions & 0 deletions tests/packages/importlib_editable/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[build-system]
requires = ["scikit-build-core"]
build-backend = "scikit_build_core.build"

[project]
name = "example"
version = "0.0.1"

[tool.scikit-build]
build-dir = "build/{wheel_tag}"
60 changes: 60 additions & 0 deletions tests/test_editable.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import sys
import textwrap
from pathlib import Path

import pytest
Expand Down Expand Up @@ -105,3 +106,62 @@ def test_cython_pxd(monkeypatch, tmp_path, editable, editable_mode, isolated):
*editable_flag,
".",
)


@pytest.mark.compile()
@pytest.mark.configure()
@pytest.mark.integration()
@pytest.mark.parametrize(
("editable", "editable_mode"), [(False, ""), (True, "redirect"), (True, "inplace")]
)
def test_importlib_resources(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:
package1 = PackageInfo(
"importlib_editable",
)
process_package(package1, 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,
".",
)

value = isolated.execute(
textwrap.dedent(
"""
import importlib.resources
import example
root = importlib.resources.files(example)
print(any('testfile' in str(x) for x in root.iterdir()))
"""
)
)
assert value == "True"
Loading