From a2f302a7c5597aa5480cb3cf512788a269826f7b Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 22 Feb 2023 21:39:30 +0000 Subject: [PATCH 01/14] Test backend is loaded from backend-path --- tests/test_inplace_hooks.py | 44 +++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/tests/test_inplace_hooks.py b/tests/test_inplace_hooks.py index d1134bf..2a703e2 100644 --- a/tests/test_inplace_hooks.py +++ b/tests/test_inplace_hooks.py @@ -1,8 +1,11 @@ +from inspect import cleandoc from os.path import abspath, dirname from os.path import join as pjoin +from pathlib import Path import pytest from testpath import modified_env +from testpath.tempdir import TemporaryDirectory from pyproject_hooks import BackendInvalid, BuildBackendHookCaller from tests.compat import tomllib @@ -61,3 +64,44 @@ def test_intree_backend_not_in_path(): with modified_env({"PYTHONPATH": BUILDSYS_PKGS}): with pytest.raises(BackendInvalid): hooks.get_requires_for_build_sdist({}) + + +def test_intree_backend_loaded_from_correct_backend_path(): + """ + PEP 517 establishes that the backend code should be loaded from ``backend-path``, + and recognizes that not always the environment isolation is perfect + (e.g. it explicitly mentions ``--system-site-packages``). Therefore, even in a + situation where a ``MetaPathFinder`` would have priority to find the backend spec, + the backend should still be loaded from ``backend-path``. + """ + hooks = get_hooks("pkg_intree", backend="intree_backend") + with TemporaryDirectory() as tmp: + invalid = Path(tmp, ".invalid", "intree_backend.py") + invalid.parent.mkdir() + invalid.write_text("raise ImportError('Do not import')", encoding="utf-8") + install_finder_with_sitecustomize(tmp, {"intree_backend": str(invalid)}) + with modified_env({"PYTHONPATH": tmp}): # Override `sitecustomize`. + res = hooks.get_requires_for_build_sdist({}) + assert res == ["intree_backend_called"] + + +def install_finder_with_sitecustomize(directory, mapping): + finder = f""" + import sys + from importlib.util import spec_from_file_location + + MAPPING = {mapping!r} + + class _Finder: # MetaPathFinder + @classmethod + def find_spec(cls, fullname, path=None, target=None): + if fullname in MAPPING: + return spec_from_file_location(fullname, MAPPING[fullname]) + + def install(): + if not any(finder == _Finder for finder in sys.meta_path): + sys.meta_path.insert(0, _Finder) + """ + sitecustomize = "import _test_finder_; _test_finder_.install()" + Path(directory, "_test_finder_.py").write_text(cleandoc(finder), encoding="utf-8") + Path(directory, "sitecustomize.py").write_text(sitecustomize, encoding="utf-8") From 275b64534de59ed115c8c3932d522bce251b5e46 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 22 Feb 2023 21:44:52 +0000 Subject: [PATCH 02/14] Actively import from backend-path instead of checking Previously `_in_process` would only modify `sys.path`, use `importlib.import_module` and hope the backend is loaded from the right path. This would lead to misleading error messages in some cases when the environment isolation is not perfect. This change modifies the behavior by using `importlib.machinery.PathFinder.find_spec` to locate the backend module within a set of path entries. --- .../_in_process/_in_process.py | 48 ++++++++++++------- tests/test_inplace_hooks.py | 5 +- 2 files changed, 34 insertions(+), 19 deletions(-) diff --git a/src/pyproject_hooks/_in_process/_in_process.py b/src/pyproject_hooks/_in_process/_in_process.py index fa0beae..7fb9406 100644 --- a/src/pyproject_hooks/_in_process/_in_process.py +++ b/src/pyproject_hooks/_in_process/_in_process.py @@ -21,6 +21,8 @@ import traceback from glob import glob from importlib import import_module +from importlib.util import module_from_spec +from importlib.machinery import PathFinder from os.path import join as pjoin # This file is run as a script, and `import wrappers` is not zip-safe, so we @@ -59,31 +61,21 @@ def __init__(self, hook_name=None): self.hook_name = hook_name -def contained_in(filename, directory): - """Test if a file is located within the given directory.""" - filename = os.path.normcase(os.path.abspath(filename)) - directory = os.path.normcase(os.path.abspath(directory)) - return os.path.commonprefix([filename, directory]) == directory - - def _build_backend(): """Find and load the build backend""" # Add in-tree backend directories to the front of sys.path. backend_path = os.environ.get("_PYPROJECT_HOOKS_BACKEND_PATH") - if backend_path: - extra_pathitems = backend_path.split(os.pathsep) - sys.path[:0] = extra_pathitems - ep = os.environ["_PYPROJECT_HOOKS_BUILD_BACKEND"] mod_path, _, obj_path = ep.partition(":") - try: - obj = import_module(mod_path) - except ImportError: - raise BackendUnavailable(traceback.format_exc()) if backend_path: - if not any(contained_in(obj.__file__, path) for path in extra_pathitems): - raise BackendInvalid("Backend was not loaded from backend-path") + extra_pathitems = backend_path.split(os.pathsep) + obj = _load_module_from_path(mod_path, extra_pathitems) + else: + try: + obj = import_module(mod_path) + except ImportError: + raise BackendUnavailable(traceback.format_exc()) if obj_path: for path_part in obj_path.split("."): @@ -91,6 +83,28 @@ def _build_backend(): return obj +def _load_module_from_path(fullname, pathitems): + """Given a set of sys.path-like entries, load a module from it""" + sys.path[:0] = pathitems # Still required for other imports. + parts = fullname.split(".") + # Parent packages need to be imported to ensure everything comes from pathitems. + for i in range(len(parts)): + module_name = ".".join(parts[: i + 1]) + spec = _find_spec_in_path(module_name, pathitems) + module = module_from_spec(spec) + sys.modules[module_name] = module + spec.loader.exec_module(module) + return module + + +def _find_spec_in_path(fullname, pathitems): + """Given sys.path-like entries, find a module spec or raise an exception""" + spec = PathFinder.find_spec(fullname, path=pathitems) + if not spec: + raise BackendUnavailable(f"Cannot find module {fullname!r} in {pathitems!r}") + return spec + + def _supported_features(): """Return the list of options features supported by the backend. diff --git a/tests/test_inplace_hooks.py b/tests/test_inplace_hooks.py index 2a703e2..53be476 100644 --- a/tests/test_inplace_hooks.py +++ b/tests/test_inplace_hooks.py @@ -7,7 +7,7 @@ from testpath import modified_env from testpath.tempdir import TemporaryDirectory -from pyproject_hooks import BackendInvalid, BuildBackendHookCaller +from pyproject_hooks import BackendUnavailable, BuildBackendHookCaller from tests.compat import tomllib SAMPLES_DIR = pjoin(dirname(abspath(__file__)), "samples") @@ -62,7 +62,8 @@ def test_intree_backend(): def test_intree_backend_not_in_path(): hooks = get_hooks("pkg_intree", backend="buildsys") with modified_env({"PYTHONPATH": BUILDSYS_PKGS}): - with pytest.raises(BackendInvalid): + msg = "Cannot find module 'buildsys' in .*/pkg_intree/backend" + with pytest.raises(BackendUnavailable, match=msg): hooks.get_requires_for_build_sdist({}) From 7a01590be25c746d93b4317e1ce5f79ba21e46ec Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 22 Feb 2023 22:16:59 +0000 Subject: [PATCH 03/14] Unify usages of BackedInvalid with BackendUnavailable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit … when error regards import problems. --- src/pyproject_hooks/_impl.py | 13 ++++++++----- src/pyproject_hooks/_in_process/_in_process.py | 16 +++++----------- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/src/pyproject_hooks/_impl.py b/src/pyproject_hooks/_impl.py index 67e0ea7..70c5d5b 100644 --- a/src/pyproject_hooks/_impl.py +++ b/src/pyproject_hooks/_impl.py @@ -23,8 +23,12 @@ def read_json(path): class BackendUnavailable(Exception): """Will be raised if the backend cannot be imported in the hook process.""" - def __init__(self, traceback): + def __init__(self, traceback, message=None, backend_name=None, backend_path=None): + # Keep API backward compatibility + self.backend_name = backend_name + self.backend_path = backend_path self.traceback = traceback + super().__init__(message or "Error while importing backend") class BackendInvalid(Exception): @@ -334,12 +338,11 @@ def _call_hook(self, hook_name, kwargs): if data.get("unsupported"): raise UnsupportedOperation(data.get("traceback", "")) if data.get("no_backend"): - raise BackendUnavailable(data.get("traceback", "")) - if data.get("backend_invalid"): - raise BackendInvalid( + raise BackendUnavailable( + data.get("traceback", ""), + message=data.get("backend_error", ""), backend_name=self.build_backend, backend_path=self.backend_path, - message=data.get("backend_error", ""), ) if data.get("hook_missing"): raise HookMissing(data.get("missing_hook_name") or hook_name) diff --git a/src/pyproject_hooks/_in_process/_in_process.py b/src/pyproject_hooks/_in_process/_in_process.py index 7fb9406..7ca9876 100644 --- a/src/pyproject_hooks/_in_process/_in_process.py +++ b/src/pyproject_hooks/_in_process/_in_process.py @@ -42,15 +42,10 @@ def read_json(path): class BackendUnavailable(Exception): """Raised if we cannot import the backend""" - def __init__(self, traceback): - self.traceback = traceback - - -class BackendInvalid(Exception): - """Raised if the backend is invalid""" - - def __init__(self, message): + def __init__(self, message, traceback=None): + super().__init__(message) self.message = message + self.traceback = traceback class HookMissing(Exception): @@ -75,7 +70,8 @@ def _build_backend(): try: obj = import_module(mod_path) except ImportError: - raise BackendUnavailable(traceback.format_exc()) + msg = "Cannot import {mod_path!r}" + raise BackendUnavailable(msg, traceback.format_exc()) if obj_path: for path_part in obj_path.split("."): @@ -356,8 +352,6 @@ def main(): except BackendUnavailable as e: json_out["no_backend"] = True json_out["traceback"] = e.traceback - except BackendInvalid as e: - json_out["backend_invalid"] = True json_out["backend_error"] = e.message except GotUnsupportedOperation as e: json_out["unsupported"] = True From bfcdac160ffc9e3201711e6c5fe985d89fad631e Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 22 Feb 2023 23:22:46 +0000 Subject: [PATCH 04/14] Fix path sep error when testing on windows --- tests/test_inplace_hooks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_inplace_hooks.py b/tests/test_inplace_hooks.py index 53be476..b94f961 100644 --- a/tests/test_inplace_hooks.py +++ b/tests/test_inplace_hooks.py @@ -62,7 +62,7 @@ def test_intree_backend(): def test_intree_backend_not_in_path(): hooks = get_hooks("pkg_intree", backend="buildsys") with modified_env({"PYTHONPATH": BUILDSYS_PKGS}): - msg = "Cannot find module 'buildsys' in .*/pkg_intree/backend" + msg = "Cannot find module 'buildsys' in .*pkg_intree.*backend" with pytest.raises(BackendUnavailable, match=msg): hooks.get_requires_for_build_sdist({}) From 148b2aa76f343617d74a2aaf698aa6a2b8533e23 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 22 Feb 2023 23:23:34 +0000 Subject: [PATCH 05/14] Improve explanation in code comment --- src/pyproject_hooks/_impl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyproject_hooks/_impl.py b/src/pyproject_hooks/_impl.py index 70c5d5b..a64c0ea 100644 --- a/src/pyproject_hooks/_impl.py +++ b/src/pyproject_hooks/_impl.py @@ -24,7 +24,7 @@ class BackendUnavailable(Exception): """Will be raised if the backend cannot be imported in the hook process.""" def __init__(self, traceback, message=None, backend_name=None, backend_path=None): - # Keep API backward compatibility + # Preserving arg order for the sake of API backward compatibility. self.backend_name = backend_name self.backend_path = backend_path self.traceback = traceback From 5726c390bbb41135608ed7094579ec997bfa90de Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 22 Feb 2023 23:31:34 +0000 Subject: [PATCH 06/14] Ensure backend info is available in exception --- src/pyproject_hooks/_in_process/_in_process.py | 2 +- tests/test_call_hooks.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/pyproject_hooks/_in_process/_in_process.py b/src/pyproject_hooks/_in_process/_in_process.py index 7ca9876..efa4e39 100644 --- a/src/pyproject_hooks/_in_process/_in_process.py +++ b/src/pyproject_hooks/_in_process/_in_process.py @@ -70,7 +70,7 @@ def _build_backend(): try: obj = import_module(mod_path) except ImportError: - msg = "Cannot import {mod_path!r}" + msg = f"Cannot import {mod_path!r}" raise BackendUnavailable(msg, traceback.format_exc()) if obj_path: diff --git a/tests/test_call_hooks.py b/tests/test_call_hooks.py index b1ba351..da410b0 100644 --- a/tests/test_call_hooks.py +++ b/tests/test_call_hooks.py @@ -34,8 +34,10 @@ def get_hooks(pkg, **kwargs): def test_missing_backend_gives_exception(): hooks = get_hooks("pkg1") with modified_env({"PYTHONPATH": ""}): - with pytest.raises(BackendUnavailable): + msg = "Cannot import 'buildsys'" + with pytest.raises(BackendUnavailable, match=msg) as exc: hooks.get_requires_for_build_wheel({}) + assert exc.value.backend_name == "buildsys" def test_get_requires_for_build_wheel(): From 4790b2b0c72a8ea90a500b28e50f00215d0152ca Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Fri, 24 Feb 2023 09:31:37 +0000 Subject: [PATCH 07/14] Replace custom import sequence with MetaPathFinder This should address review comments and increase the robustness of the solution. --- .../_in_process/_in_process.py | 56 +++++++++---------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/src/pyproject_hooks/_in_process/_in_process.py b/src/pyproject_hooks/_in_process/_in_process.py index efa4e39..f9db9e1 100644 --- a/src/pyproject_hooks/_in_process/_in_process.py +++ b/src/pyproject_hooks/_in_process/_in_process.py @@ -21,7 +21,6 @@ import traceback from glob import glob from importlib import import_module -from importlib.util import module_from_spec from importlib.machinery import PathFinder from os.path import join as pjoin @@ -65,13 +64,13 @@ def _build_backend(): if backend_path: extra_pathitems = backend_path.split(os.pathsep) - obj = _load_module_from_path(mod_path, extra_pathitems) - else: - try: - obj = import_module(mod_path) - except ImportError: - msg = f"Cannot import {mod_path!r}" - raise BackendUnavailable(msg, traceback.format_exc()) + sys.meta_path.insert(0, _BackendPathFinder(extra_pathitems, mod_path)) + + try: + obj = import_module(mod_path) + except ImportError: + msg = f"Cannot import {mod_path!r}" + raise BackendUnavailable(msg, traceback.format_exc()) if obj_path: for path_part in obj_path.split("."): @@ -79,26 +78,27 @@ def _build_backend(): return obj -def _load_module_from_path(fullname, pathitems): - """Given a set of sys.path-like entries, load a module from it""" - sys.path[:0] = pathitems # Still required for other imports. - parts = fullname.split(".") - # Parent packages need to be imported to ensure everything comes from pathitems. - for i in range(len(parts)): - module_name = ".".join(parts[: i + 1]) - spec = _find_spec_in_path(module_name, pathitems) - module = module_from_spec(spec) - sys.modules[module_name] = module - spec.loader.exec_module(module) - return module - - -def _find_spec_in_path(fullname, pathitems): - """Given sys.path-like entries, find a module spec or raise an exception""" - spec = PathFinder.find_spec(fullname, path=pathitems) - if not spec: - raise BackendUnavailable(f"Cannot find module {fullname!r} in {pathitems!r}") - return spec +class _BackendPathFinder: + """Implements the MetaPathFinder interface to locate modules in ``backend-path``. + + Since the environment provided by the frontend can contain all sorts of + MetaPathFinders, the only way to ensure the backend is loaded from the + right place is to prepend our own. + """ + + def __init__(self, backend_path, backend_module): + self.backend_path = backend_path + self.backend_module = backend_module + + def find_spec(self, fullname, _path, _target=None): + # Ignore other items in _path or sys.path and use backend_path instead: + spec = PathFinder.find_spec(fullname, path=self.backend_path) + if spec is None and fullname == self.backend_module: + # According to the spec, the backend MUST be loaded from backend-path. + # Therefore, we can halt the import machinery and raise a clean error. + msg = f"Cannot find module {self.backend_module!r} in {self.backend_path!r}" + raise BackendUnavailable(msg) + return spec def _supported_features(): From dcdedc9a6bc491b60bcd83a1bfacf02e62eb0977 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Fri, 24 Feb 2023 10:08:55 +0000 Subject: [PATCH 08/14] Improve test docstring for intree backend --- tests/test_inplace_hooks.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/test_inplace_hooks.py b/tests/test_inplace_hooks.py index b94f961..5087030 100644 --- a/tests/test_inplace_hooks.py +++ b/tests/test_inplace_hooks.py @@ -71,9 +71,10 @@ def test_intree_backend_loaded_from_correct_backend_path(): """ PEP 517 establishes that the backend code should be loaded from ``backend-path``, and recognizes that not always the environment isolation is perfect - (e.g. it explicitly mentions ``--system-site-packages``). Therefore, even in a - situation where a ``MetaPathFinder`` would have priority to find the backend spec, - the backend should still be loaded from ``backend-path``. + (e.g. it explicitly mentions ``--system-site-packages``). + Therefore, even in a situation where a third-party ``MetaPathFinder`` has + precedence over ``importlib.machinery.PathFinder``, the backend should + still be loaded from ``backend-path``. """ hooks = get_hooks("pkg_intree", backend="intree_backend") with TemporaryDirectory() as tmp: From 0177e3d69817fc0b303cb751b6c54a2d5bbb7407 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Fri, 24 Feb 2023 10:14:01 +0000 Subject: [PATCH 09/14] Replace comment with a more appropriate description --- src/pyproject_hooks/_in_process/_in_process.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyproject_hooks/_in_process/_in_process.py b/src/pyproject_hooks/_in_process/_in_process.py index f9db9e1..3b6e2e7 100644 --- a/src/pyproject_hooks/_in_process/_in_process.py +++ b/src/pyproject_hooks/_in_process/_in_process.py @@ -57,12 +57,12 @@ def __init__(self, hook_name=None): def _build_backend(): """Find and load the build backend""" - # Add in-tree backend directories to the front of sys.path. backend_path = os.environ.get("_PYPROJECT_HOOKS_BACKEND_PATH") ep = os.environ["_PYPROJECT_HOOKS_BUILD_BACKEND"] mod_path, _, obj_path = ep.partition(":") if backend_path: + # Ensure in-tree backend directories have the highest priority when importing. extra_pathitems = backend_path.split(os.pathsep) sys.meta_path.insert(0, _BackendPathFinder(extra_pathitems, mod_path)) From 7bb74e23ace417545a6c34459b3b42b1199a1c0d Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Sun, 6 Aug 2023 17:14:46 +0100 Subject: [PATCH 10/14] Mark BackendInvalid as no longer used/deprecated --- src/pyproject_hooks/_impl.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/pyproject_hooks/_impl.py b/src/pyproject_hooks/_impl.py index a64c0ea..bed333a 100644 --- a/src/pyproject_hooks/_impl.py +++ b/src/pyproject_hooks/_impl.py @@ -32,7 +32,14 @@ def __init__(self, traceback, message=None, backend_name=None, backend_path=None class BackendInvalid(Exception): - """Will be raised if the backend is invalid.""" + """Will be raised if the backend is invalid. + + .. deprecated:: 1.1.0 + ``pyproject_hooks`` no longer produces ``BackendInvalid`` exceptions. + Consider using ``BackendUnavailable`` to handle situations that + previously would raise ``BackendInvalid``. + Future versions of the library may remove this class. + """ def __init__(self, backend_name, backend_path, message): super().__init__(message) From a318f8ed39b9f01d4c81066bbf4966c377a613b2 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Tue, 8 Aug 2023 16:06:45 +0100 Subject: [PATCH 11/14] Remove unused BackendInvalid --- docs/pyproject_hooks.rst | 1 - src/pyproject_hooks/__init__.py | 2 -- src/pyproject_hooks/_impl.py | 16 ---------------- 3 files changed, 19 deletions(-) diff --git a/docs/pyproject_hooks.rst b/docs/pyproject_hooks.rst index 2e4b682..3acf7dc 100644 --- a/docs/pyproject_hooks.rst +++ b/docs/pyproject_hooks.rst @@ -39,7 +39,6 @@ Exceptions Each exception has public attributes with the same name as their constructors. -.. autoexception:: pyproject_hooks.BackendInvalid .. autoexception:: pyproject_hooks.BackendUnavailable .. autoexception:: pyproject_hooks.HookMissing .. autoexception:: pyproject_hooks.UnsupportedOperation diff --git a/src/pyproject_hooks/__init__.py b/src/pyproject_hooks/__init__.py index 9ae51a8..38a223e 100644 --- a/src/pyproject_hooks/__init__.py +++ b/src/pyproject_hooks/__init__.py @@ -2,7 +2,6 @@ """ from ._impl import ( - BackendInvalid, BackendUnavailable, BuildBackendHookCaller, HookMissing, @@ -14,7 +13,6 @@ __version__ = "1.0.0" __all__ = [ "BackendUnavailable", - "BackendInvalid", "HookMissing", "UnsupportedOperation", "default_subprocess_runner", diff --git a/src/pyproject_hooks/_impl.py b/src/pyproject_hooks/_impl.py index bed333a..c0511a0 100644 --- a/src/pyproject_hooks/_impl.py +++ b/src/pyproject_hooks/_impl.py @@ -31,22 +31,6 @@ def __init__(self, traceback, message=None, backend_name=None, backend_path=None super().__init__(message or "Error while importing backend") -class BackendInvalid(Exception): - """Will be raised if the backend is invalid. - - .. deprecated:: 1.1.0 - ``pyproject_hooks`` no longer produces ``BackendInvalid`` exceptions. - Consider using ``BackendUnavailable`` to handle situations that - previously would raise ``BackendInvalid``. - Future versions of the library may remove this class. - """ - - def __init__(self, backend_name, backend_path, message): - super().__init__(message) - self.backend_name = backend_name - self.backend_path = backend_path - - class HookMissing(Exception): """Will be raised on missing hooks (if a fallback can't be used).""" From 32c6af37e7dffc67c03118c4943e2bccc56c81b0 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Fri, 18 Aug 2023 13:40:52 +0100 Subject: [PATCH 12/14] Test nested intree backend --- tests/samples/pkg_nested_intree/backend/intree_backend.py | 3 +++ .../pkg_nested_intree/backend/nested/intree_backend.py | 2 ++ tests/samples/pkg_nested_intree/pyproject.toml | 3 +++ tests/test_inplace_hooks.py | 5 +++-- 4 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 tests/samples/pkg_nested_intree/backend/intree_backend.py create mode 100644 tests/samples/pkg_nested_intree/backend/nested/intree_backend.py create mode 100644 tests/samples/pkg_nested_intree/pyproject.toml diff --git a/tests/samples/pkg_nested_intree/backend/intree_backend.py b/tests/samples/pkg_nested_intree/backend/intree_backend.py new file mode 100644 index 0000000..17c9232 --- /dev/null +++ b/tests/samples/pkg_nested_intree/backend/intree_backend.py @@ -0,0 +1,3 @@ +# PathFinder.find_spec only take into consideration the last segment +# of the module name (not the full name). +raise Exception("This isn't the backend you are looking for") diff --git a/tests/samples/pkg_nested_intree/backend/nested/intree_backend.py b/tests/samples/pkg_nested_intree/backend/nested/intree_backend.py new file mode 100644 index 0000000..a10e1e2 --- /dev/null +++ b/tests/samples/pkg_nested_intree/backend/nested/intree_backend.py @@ -0,0 +1,2 @@ +def get_requires_for_build_sdist(config_settings): + return ["intree_backend_called"] diff --git a/tests/samples/pkg_nested_intree/pyproject.toml b/tests/samples/pkg_nested_intree/pyproject.toml new file mode 100644 index 0000000..1b7eb83 --- /dev/null +++ b/tests/samples/pkg_nested_intree/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +build-backend = 'nested.intree_backend' +backend-path = ['backend'] diff --git a/tests/test_inplace_hooks.py b/tests/test_inplace_hooks.py index 5087030..3f05920 100644 --- a/tests/test_inplace_hooks.py +++ b/tests/test_inplace_hooks.py @@ -52,8 +52,9 @@ def test_backend_out_of_tree(backend_path): BuildBackendHookCaller(SOURCE_DIR, "dummy", backend_path) -def test_intree_backend(): - hooks = get_hooks("pkg_intree") +@pytest.mark.parametrize("example", ("pkg_intree", "pkg_nested_intree")) +def test_intree_backend(example): + hooks = get_hooks(example) with modified_env({"PYTHONPATH": BUILDSYS_PKGS}): res = hooks.get_requires_for_build_sdist({}) assert res == ["intree_backend_called"] From 19023eb70e9cf6aedce89f64e41b03973a90ed79 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Fri, 18 Aug 2023 13:58:26 +0100 Subject: [PATCH 13/14] Add negative tests for nested intree backends --- tests/samples/buildsys_pkgs/nested/buildsys.py | 1 + tests/test_inplace_hooks.py | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) create mode 100644 tests/samples/buildsys_pkgs/nested/buildsys.py diff --git a/tests/samples/buildsys_pkgs/nested/buildsys.py b/tests/samples/buildsys_pkgs/nested/buildsys.py new file mode 100644 index 0000000..b8e37ac --- /dev/null +++ b/tests/samples/buildsys_pkgs/nested/buildsys.py @@ -0,0 +1 @@ +from ..buildsys import * # noqa: F403 diff --git a/tests/test_inplace_hooks.py b/tests/test_inplace_hooks.py index 3f05920..6b73ceb 100644 --- a/tests/test_inplace_hooks.py +++ b/tests/test_inplace_hooks.py @@ -60,10 +60,11 @@ def test_intree_backend(example): assert res == ["intree_backend_called"] -def test_intree_backend_not_in_path(): - hooks = get_hooks("pkg_intree", backend="buildsys") +@pytest.mark.parametrize("backend", ("buildsys", "nested.buildsys")) +def test_intree_backend_not_in_path(backend): + hooks = get_hooks("pkg_intree", backend=backend) with modified_env({"PYTHONPATH": BUILDSYS_PKGS}): - msg = "Cannot find module 'buildsys' in .*pkg_intree.*backend" + msg = f"Cannot find module {backend!r} in .*pkg_intree.*backend" with pytest.raises(BackendUnavailable, match=msg): hooks.get_requires_for_build_sdist({}) From 89722cbdb9f9ddef2a34e073823096b060dc353e Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Fri, 18 Aug 2023 14:00:19 +0100 Subject: [PATCH 14/14] Avoid passing nested modules to PathFinder --- src/pyproject_hooks/_in_process/_in_process.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/pyproject_hooks/_in_process/_in_process.py b/src/pyproject_hooks/_in_process/_in_process.py index 3b6e2e7..49c4203 100644 --- a/src/pyproject_hooks/_in_process/_in_process.py +++ b/src/pyproject_hooks/_in_process/_in_process.py @@ -89,15 +89,21 @@ class _BackendPathFinder: def __init__(self, backend_path, backend_module): self.backend_path = backend_path self.backend_module = backend_module + self.backend_parent, _, _ = backend_module.partition(".") def find_spec(self, fullname, _path, _target=None): + if "." in fullname: + # Rely on importlib to find nested modules based on parent's path + return None + # Ignore other items in _path or sys.path and use backend_path instead: spec = PathFinder.find_spec(fullname, path=self.backend_path) - if spec is None and fullname == self.backend_module: + if spec is None and fullname == self.backend_parent: # According to the spec, the backend MUST be loaded from backend-path. # Therefore, we can halt the import machinery and raise a clean error. msg = f"Cannot find module {self.backend_module!r} in {self.backend_path!r}" raise BackendUnavailable(msg) + return spec