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/emod.pyi b/tests/packages/importlib_editable/emod.pyi new file mode 100644 index 00000000..44914bb4 --- /dev/null +++ b/tests/packages/importlib_editable/emod.pyi @@ -0,0 +1 @@ +def square(x: float) -> float: ... 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..b13f9db0 --- /dev/null +++ b/tests/packages/importlib_editable/pkg/__init__.py @@ -0,0 +1,32 @@ +# Don't let ruff sort imports in this file, we want to keep them with the comments as is +# for clarity. +# ruff: noqa: I001, F401 +# mypy: ignore-errors + +# Level zero import global modules +from pmod import square as psquare +from emod import square as esquare + +# Level one pure modules +from .pmod_a import square as psquare_a + +# Level one extension modules +from .emod_a import square as esquare_a + +# Level one subpackages +from . import sub_a + +# Level two pure modules +from .sub_a.pmod_b import square as psquare_b + +# Level two extension modules +from .sub_a.emod_b import square as esquare_b + +# Level two subpackages +from .sub_b import sub_c + +# Level three pure modules +from .sub_b.sub_c.pmod_d import square as psquare_d + +# Level three extension modules +from .sub_b.sub_c.emod_d import square as esquare_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..44914bb4 --- /dev/null +++ b/tests/packages/importlib_editable/pkg/emod_a.pyi @@ -0,0 +1 @@ +def square(x: float) -> float: ... 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..c84f2b71 --- /dev/null +++ b/tests/packages/importlib_editable/pkg/pmod_a.py @@ -0,0 +1,9 @@ +# ruff: noqa: I001, F401 +# mypy: ignore-errors + +# Level one import sibling +from .emod_a import square as esquare + + +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..c7e2757d --- /dev/null +++ b/tests/packages/importlib_editable/pkg/sub_a/__init__.py @@ -0,0 +1,15 @@ +# ruff: noqa: I001, F401 +# mypy: ignore-errors + +from .pmod_b import square as psquare +from .emod_b import square as esquare + +# Level one import cousin +from .. import sub_b +from ..sub_b.pmod_c import square as psquare_c +from ..sub_b.emod_c import square as esquare_c + +# Level one import distant cousin +from ..sub_b import sub_c +from ..sub_b.sub_c.pmod_d import square as psquare_d +from ..sub_b.sub_c.emod_d import square as esquare_d 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..44914bb4 --- /dev/null +++ b/tests/packages/importlib_editable/pkg/sub_a/emod_b.pyi @@ -0,0 +1 @@ +def square(x: float) -> float: ... 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..d1e1290a --- /dev/null +++ b/tests/packages/importlib_editable/pkg/sub_a/pmod_b.py @@ -0,0 +1,9 @@ +# ruff: noqa: I001, F401 +# mypy: ignore-errors + +# Level two import sibling +from .emod_b import square as esquare + + +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..dec028c4 --- /dev/null +++ b/tests/packages/importlib_editable/pkg/sub_b/__init__.py @@ -0,0 +1,19 @@ +# Don't let ruff sort imports in this file, we want to keep them with the comments as is +# for clarity. +# ruff: noqa: I001, F401 +# mypy: ignore-errors + +# Level one pure modules +from .pmod_c import square as psquare_c + +# Level one extension modules +from .emod_c import square as esquare_c + +# Level one subpackages +from . import sub_c + +# Level two pure modules +from .sub_c.pmod_d import square as psquare_d + +# Level two extension modules +from .sub_c.emod_d import square as esquare_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..44914bb4 --- /dev/null +++ b/tests/packages/importlib_editable/pkg/sub_b/emod_c.pyi @@ -0,0 +1 @@ +def square(x: float) -> float: ... 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..ca62c140 --- /dev/null +++ b/tests/packages/importlib_editable/pkg/sub_b/pmod_c.py @@ -0,0 +1,9 @@ +# ruff: noqa: I001, F401 +# mypy: ignore-errors + +# Level two import sibling +from .emod_c import square as esquare + + +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..a60a12cc --- /dev/null +++ b/tests/packages/importlib_editable/pkg/sub_b/sub_c/__init__.py @@ -0,0 +1,10 @@ +# ruff: noqa: I001, F401 +# mypy: ignore-errors + +from .pmod_d import square as psquare_d +from .emod_d import square as esquare_d + +# Level one import cousin +from .. import sub_d +from ..sub_d.pmod_e import square as psquare_e +from ..sub_d.emod_e import square as esquare_e 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..44914bb4 --- /dev/null +++ b/tests/packages/importlib_editable/pkg/sub_b/sub_c/emod_d.pyi @@ -0,0 +1 @@ +def square(x: float) -> float: ... 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..ea36e2de --- /dev/null +++ b/tests/packages/importlib_editable/pkg/sub_b/sub_c/pmod_d.py @@ -0,0 +1,9 @@ +# ruff: noqa: I001, F401 +# mypy: ignore-errors + +# Level three import sibling +from .emod_d import square as esquare + + +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..6b0f91a5 --- /dev/null +++ b/tests/packages/importlib_editable/pkg/sub_b/sub_d/__init__.py @@ -0,0 +1,5 @@ +# ruff: noqa: I001, F401 +# mypy: ignore-errors + +from .pmod_e import square as psquare_e +from .emod_e import square as esquare_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..44914bb4 --- /dev/null +++ b/tests/packages/importlib_editable/pkg/sub_b/sub_d/emod_e.pyi @@ -0,0 +1 @@ +def square(x: float) -> float: ... 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..ab44cd5d --- /dev/null +++ b/tests/packages/importlib_editable/pkg/sub_b/sub_d/pmod_e.py @@ -0,0 +1,9 @@ +# ruff: noqa: I001, F401 +# mypy: ignore-errors + +# Level three import sibling +from .emod_e import square as esquare + + +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..ee44ae39 --- /dev/null +++ b/tests/packages/importlib_editable/pmod.py @@ -0,0 +1,9 @@ +# ruff: noqa: I001, F401 +# mypy: ignore-errors + +# Level zero import global sibling +from emod import square as esquare + + +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..57368605 100644 --- a/tests/test_editable.py +++ b/tests/test_editable.py @@ -1,3 +1,4 @@ +import platform import sys import textwrap from pathlib import Path @@ -156,3 +157,128 @@ 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): + # TODO: Investigate these failures + if platform.system() == "Windows" and editable_mode == "inplace": + pytest.xfail("Windows fails to import the top-level extension module") + + _setup_package_for_editable_layout_tests( # type: ignore[no-untyped-call] + monkeypatch, tmp_path, editable, editable_mode, isolated + ) + isolated.execute("import pkg") + isolated.execute("import pmod") + isolated.execute("import emod") + + +@pytest.mark.compile +@pytest.mark.configure +@pytest.mark.integration +@pytest.mark.parametrize( + ("editable", "editable_mode"), + [ + (False, ""), + pytest.param( + True, + "redirect", + marks=pytest.mark.xfail, + ), + (True, "inplace"), + ], +) +def test_importlib_resources(monkeypatch, tmp_path, editable, editable_mode, isolated): + if sys.version_info < (3, 9): + pytest.skip("importlib.resources.files is introduced in Python 3.9") + + # TODO: Investigate these failures + if editable_mode == "redirect": + pytest.xfail("Redirect mode is at navigating importlib.resources.files") + if platform.system() == "Windows" and editable_mode == "inplace": + pytest.xfail("Windows fails to import the top-level extension module") + + _setup_package_for_editable_layout_tests( + monkeypatch, tmp_path, editable, editable_mode, isolated + ) + + isolated.execute( + textwrap.dedent( + """ + from importlib import import_module + from importlib.resources import files + from pathlib import Path + + def is_extension(path): + for ext in (".so", ".pyd"): + if ext in path.suffixes: + return True + return False + + def check_pkg(pkg_name): + try: + pkg = import_module(pkg_name) + pkg_root = files(pkg) + print(f"pkg_root: [{type(pkg_root)}] {pkg_root}") + pkg_files = list(pkg_root.iterdir()) + for path in pkg_files: + print(f"path: [{type(path)}] {path}") + assert any(is_extension(path) for path in pkg_files if isinstance(path, Path)) + except Exception as err: + msg = f"Failed in {str(pkg)}" + raise RuntimeError(msg) from err + + check_pkg("pkg") + check_pkg("pkg.sub_a") + check_pkg("pkg.sub_b") + check_pkg("pkg.sub_b.sub_c") + check_pkg("pkg.sub_b.sub_d") + """ + ) + )