From d362431d023c92d16d05430a210335efdb94885d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Sat, 2 Oct 2021 18:04:56 +0200 Subject: [PATCH 1/7] Add TODO --- src/pip/_internal/operations/prepare.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index ccf034eb5a0..34cf9a51b6e 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -348,6 +348,7 @@ def _ensure_link_req_src_dir( # installation. # FIXME: this won't upgrade when there's an existing # package unpacked in `req.source_dir` + # TODO: this check is now probably dead code if is_installable_dir(req.source_dir): raise PreviousBuildDirError( "pip can't proceed with requirements '{}' due to a" From ddfeaaef2096cf21dda6622f0d8807bb10ed2a1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Sat, 2 Oct 2021 18:06:07 +0200 Subject: [PATCH 2/7] Reject projects that have neither a pyproject.toml nor a setup.py --- news/10531.bugfix.rst | 2 ++ src/pip/_internal/pyproject.py | 6 ++++++ src/pip/_internal/req/constructors.py | 2 ++ tests/data/src/pep517_setup_cfg_only/setup.cfg | 3 +++ tests/functional/test_install.py | 10 ++++------ tests/unit/test_pep517.py | 16 ++++++++++++++++ 6 files changed, 33 insertions(+), 6 deletions(-) create mode 100644 news/10531.bugfix.rst create mode 100644 tests/data/src/pep517_setup_cfg_only/setup.cfg diff --git a/news/10531.bugfix.rst b/news/10531.bugfix.rst new file mode 100644 index 00000000000..0a33944a10d --- /dev/null +++ b/news/10531.bugfix.rst @@ -0,0 +1,2 @@ +Always refuse installing or building projects that have no ``pyproject.toml`` nor +``setup.py``. diff --git a/src/pip/_internal/pyproject.py b/src/pip/_internal/pyproject.py index 0b3a6cde64f..31534a3a9d3 100644 --- a/src/pip/_internal/pyproject.py +++ b/src/pip/_internal/pyproject.py @@ -48,6 +48,12 @@ def load_pyproject_toml( has_pyproject = os.path.isfile(pyproject_toml) has_setup = os.path.isfile(setup_py) + if not has_pyproject and not has_setup: + raise InstallationError( + f"{req_name} does not appear to be a Python project: " + f"neither 'setup.py' nor 'pyproject.toml' found." + ) + if has_pyproject: with open(pyproject_toml, encoding="utf-8") as f: pp_toml = tomli.load(f) diff --git a/src/pip/_internal/req/constructors.py b/src/pip/_internal/req/constructors.py index 5cf923515d7..4a594037fd1 100644 --- a/src/pip/_internal/req/constructors.py +++ b/src/pip/_internal/req/constructors.py @@ -235,6 +235,8 @@ def _get_url_from_path(path: str, name: str) -> Optional[str]: if _looks_like_path(name) and os.path.isdir(path): if is_installable_dir(path): return path_to_url(path) + # TODO: The is_installable_dir test here might not be necessary + # now that it is done in load_pyproject_toml too. raise InstallationError( f"Directory {name!r} is not installable. Neither 'setup.py' " "nor 'pyproject.toml' found." diff --git a/tests/data/src/pep517_setup_cfg_only/setup.cfg b/tests/data/src/pep517_setup_cfg_only/setup.cfg new file mode 100644 index 00000000000..4d62ef58d5a --- /dev/null +++ b/tests/data/src/pep517_setup_cfg_only/setup.cfg @@ -0,0 +1,3 @@ +[metadata] +name = "dummy" +version = "0.1" diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 355113e37d3..d9ceba3b721 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -653,7 +653,6 @@ def test_install_from_local_directory_with_no_setup_py(script, data): """ result = script.pip("install", data.root, expect_error=True) assert not result.files_created - assert "is not installable." in result.stderr assert "Neither 'setup.py' nor 'pyproject.toml' found." in result.stderr @@ -663,11 +662,10 @@ def test_editable_install__local_dir_no_setup_py(script, data): """ result = script.pip("install", "-e", data.root, expect_error=True) assert not result.files_created - - msg = result.stderr - assert msg.startswith("ERROR: File 'setup.py' or 'setup.cfg' not found ") - assert "cannot be installed in editable mode" in msg - assert "pyproject.toml" not in msg + assert ( + "does not appear to be a Python project: " + "neither 'setup.py' nor 'pyproject.toml' found" in result.stderr + ) def test_editable_install__local_dir_no_setup_py_with_pyproject(script): diff --git a/tests/unit/test_pep517.py b/tests/unit/test_pep517.py index b18299d7039..6c11ab625ec 100644 --- a/tests/unit/test_pep517.py +++ b/tests/unit/test_pep517.py @@ -27,6 +27,22 @@ def test_use_pep517(shared_data: TestData, source: str, expected: bool) -> None: assert req.use_pep517 is expected +def test_use_pep517_rejects_setup_cfg_only(shared_data: TestData) -> None: + """ + Test that projects with setup.cfg but no pyproject.toml are rejected. + """ + src = shared_data.src.joinpath("pep517_setup_cfg_only") + req = InstallRequirement(None, None) + req.source_dir = src # make req believe it has been unpacked + with pytest.raises(InstallationError) as e: + req.load_pyproject_toml() + err_msg = e.value.args[0] + assert ( + "does not appear to be a Python project: " + "neither 'setup.py' nor 'pyproject.toml' found" in err_msg + ) + + @pytest.mark.parametrize( ("source", "msg"), [ From 619b257ed99de8363cb16107b96dc0657501150c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Tue, 12 Oct 2021 13:56:33 +0200 Subject: [PATCH 3/7] Add failing tests for issue 10573 --- tests/functional/test_install.py | 20 ++++++++++++++++++++ tests/functional/test_pep660.py | 13 ++++++++++--- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index d9ceba3b721..ac96b1f940e 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -687,6 +687,26 @@ def test_editable_install__local_dir_no_setup_py_with_pyproject(script): assert "cannot be installed in editable mode" in msg +def test_editable_install__local_dir_setup_requires_with_pyproject(script, shared_data): + """ + Test installing in editable mode from a local directory with a setup.py + that has setup_requires and a pyproject.toml. + + https://github.com/pypa/pip/issues/10573 + """ + local_dir = script.scratch_path.joinpath("temp") + local_dir.mkdir() + pyproject_path = local_dir.joinpath("pyproject.toml") + pyproject_path.write_text("") + setup_py_path = local_dir.joinpath("setup.py") + setup_py_path.write_text( + "from setuptools import setup\n" + "setup(name='dummy', setup_requires=['simplewheel'])\n" + ) + + script.pip("install", "--find-links", shared_data.find_links, "-e", local_dir) + + @pytest.mark.network def test_upgrade_argparse_shadowed(script): # If argparse is installed - even if shadowed for imported - we support diff --git a/tests/functional/test_pep660.py b/tests/functional/test_pep660.py index 0c1ac746a91..52549f1d3f1 100644 --- a/tests/functional/test_pep660.py +++ b/tests/functional/test_pep660.py @@ -59,19 +59,22 @@ def build_editable(wheel_directory, config_settings=None, metadata_directory=Non # fmt: on -def _make_project(tmpdir, backend_code, with_setup_py): +def _make_project(tmpdir, backend_code, with_setup_py, with_pyproject=True): project_dir = tmpdir / "project" project_dir.mkdir() project_dir.joinpath("setup.cfg").write_text(SETUP_CFG) if with_setup_py: project_dir.joinpath("setup.py").write_text(SETUP_PY) if backend_code: + assert with_pyproject buildsys = {"requires": ["setuptools", "wheel"]} buildsys["build-backend"] = "test_backend" buildsys["backend-path"] = ["."] data = tomli_w.dumps({"build-system": buildsys}) project_dir.joinpath("pyproject.toml").write_text(data) project_dir.joinpath("test_backend.py").write_text(backend_code) + elif with_pyproject: + project_dir.joinpath("pyproject.toml").touch() project_dir.joinpath("log.txt").touch() return project_dir @@ -124,7 +127,8 @@ def test_install_pep660_basic(tmpdir, script, with_wheel): def test_install_no_pep660_setup_py_fallback(tmpdir, script, with_wheel): """ Test that we fall back to setuptools develop when using a backend that - does not support build_editable . + does not support build_editable. Since there is a pyproject.toml, + the prepare_metadata_for_build_wheel hook is called. """ project_dir = _make_project(tmpdir, BACKEND_WITHOUT_PEP660, with_setup_py=True) result = script.pip( @@ -135,6 +139,7 @@ def test_install_no_pep660_setup_py_fallback(tmpdir, script, with_wheel): project_dir, allow_stderr_warning=False, ) + _assert_hook_called(project_dir, "prepare_metadata_for_build_wheel") assert ( result.test_env.site_packages.joinpath("project.egg-link") in result.files_created @@ -144,7 +149,8 @@ def test_install_no_pep660_setup_py_fallback(tmpdir, script, with_wheel): def test_install_no_pep660_setup_cfg_fallback(tmpdir, script, with_wheel): """ Test that we fall back to setuptools develop when using a backend that - does not support build_editable . + does not support build_editable. Since there is a pyproject.toml, + the prepare_metadata_for_build_wheel hook is called. """ project_dir = _make_project(tmpdir, BACKEND_WITHOUT_PEP660, with_setup_py=False) result = script.pip( @@ -156,6 +162,7 @@ def test_install_no_pep660_setup_cfg_fallback(tmpdir, script, with_wheel): allow_stderr_warning=False, ) print(result.stdout, result.stderr) + _assert_hook_called(project_dir, "prepare_metadata_for_build_wheel") assert ( result.test_env.site_packages.joinpath("project.egg-link") in result.files_created From b0eb95bab97bea66c8b2d72ec17d152eb7cce130 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Mon, 18 Oct 2021 14:32:06 +0200 Subject: [PATCH 4/7] Upgrade pep517 to 0.12.0 --- news/pep517.vendor.rst | 1 + src/pip/_vendor/pep517/__init__.py | 2 +- src/pip/_vendor/pep517/build.py | 2 +- src/pip/_vendor/pep517/check.py | 2 +- src/pip/_vendor/pep517/compat.py | 11 ++++++++++- src/pip/_vendor/pep517/envbuild.py | 2 +- src/pip/_vendor/pep517/in_process/_in_process.py | 14 ++++++++++++++ src/pip/_vendor/pep517/wrappers.py | 4 ++++ src/pip/_vendor/vendor.txt | 2 +- 9 files changed, 34 insertions(+), 6 deletions(-) create mode 100644 news/pep517.vendor.rst diff --git a/news/pep517.vendor.rst b/news/pep517.vendor.rst new file mode 100644 index 00000000000..2f9070b9cdd --- /dev/null +++ b/news/pep517.vendor.rst @@ -0,0 +1 @@ +Upgrade pep517 to 0.12.0 diff --git a/src/pip/_vendor/pep517/__init__.py b/src/pip/_vendor/pep517/__init__.py index f064d60c8b9..2b6b8856790 100644 --- a/src/pip/_vendor/pep517/__init__.py +++ b/src/pip/_vendor/pep517/__init__.py @@ -1,6 +1,6 @@ """Wrappers to build Python packages using PEP 517 hooks """ -__version__ = '0.11.0' +__version__ = '0.12.0' from .wrappers import * # noqa: F401, F403 diff --git a/src/pip/_vendor/pep517/build.py b/src/pip/_vendor/pep517/build.py index 3b752145322..bc463b2ba6d 100644 --- a/src/pip/_vendor/pep517/build.py +++ b/src/pip/_vendor/pep517/build.py @@ -31,7 +31,7 @@ def load_system(source_dir): Load the build system from a source dir (pyproject.toml). """ pyproject = os.path.join(source_dir, 'pyproject.toml') - with io.open(pyproject, encoding="utf-8") as f: + with io.open(pyproject, 'rb') as f: pyproject_data = toml_load(f) return pyproject_data['build-system'] diff --git a/src/pip/_vendor/pep517/check.py b/src/pip/_vendor/pep517/check.py index 719be04033f..bf3c722641e 100644 --- a/src/pip/_vendor/pep517/check.py +++ b/src/pip/_vendor/pep517/check.py @@ -142,7 +142,7 @@ def check(source_dir): return False try: - with io.open(pyproject, encoding="utf-8") as f: + with io.open(pyproject, 'rb') as f: pyproject_data = toml_load(f) # Ensure the mandatory data can be loaded buildsys = pyproject_data['build-system'] diff --git a/src/pip/_vendor/pep517/compat.py b/src/pip/_vendor/pep517/compat.py index d5636645bd3..730ef5ffaa1 100644 --- a/src/pip/_vendor/pep517/compat.py +++ b/src/pip/_vendor/pep517/compat.py @@ -1,4 +1,5 @@ """Python 2/3 compatibility""" +import io import json import sys @@ -35,7 +36,15 @@ def read_json(path): if sys.version_info < (3, 6): - from toml import load as toml_load # noqa: F401 + from toml import load as _toml_load # noqa: F401 + + def toml_load(f): + w = io.TextIOWrapper(f, encoding="utf8", newline="") + try: + return _toml_load(w) + finally: + w.detach() + from toml import TomlDecodeError as TOMLDecodeError # noqa: F401 else: from pip._vendor.tomli import load as toml_load # noqa: F401 diff --git a/src/pip/_vendor/pep517/envbuild.py b/src/pip/_vendor/pep517/envbuild.py index 7c2344bf3bf..fe8873c64a9 100644 --- a/src/pip/_vendor/pep517/envbuild.py +++ b/src/pip/_vendor/pep517/envbuild.py @@ -19,7 +19,7 @@ def _load_pyproject(source_dir): with io.open( os.path.join(source_dir, 'pyproject.toml'), - encoding="utf-8", + 'rb', ) as f: pyproject_data = toml_load(f) buildsys = pyproject_data['build-system'] diff --git a/src/pip/_vendor/pep517/in_process/_in_process.py b/src/pip/_vendor/pep517/in_process/_in_process.py index c7f5f0577f8..954a4ab05e9 100644 --- a/src/pip/_vendor/pep517/in_process/_in_process.py +++ b/src/pip/_vendor/pep517/in_process/_in_process.py @@ -103,6 +103,19 @@ def _build_backend(): return obj +def _supported_features(): + """Return the list of options features supported by the backend. + + Returns a list of strings. + The only possible value is 'build_editable'. + """ + backend = _build_backend() + features = [] + if hasattr(backend, "build_editable"): + features.append("build_editable") + return features + + def get_requires_for_build_wheel(config_settings): """Invoke the optional get_requires_for_build_wheel hook @@ -312,6 +325,7 @@ def build_sdist(sdist_directory, config_settings): 'build_editable', 'get_requires_for_build_sdist', 'build_sdist', + '_supported_features', } diff --git a/src/pip/_vendor/pep517/wrappers.py b/src/pip/_vendor/pep517/wrappers.py index 52da22e8257..e031ed70875 100644 --- a/src/pip/_vendor/pep517/wrappers.py +++ b/src/pip/_vendor/pep517/wrappers.py @@ -154,6 +154,10 @@ def subprocess_runner(self, runner): finally: self._subprocess_runner = prev + def _supported_features(self): + """Return the list of optional features supported by the backend.""" + return self._call_hook('_supported_features', {}) + def get_requires_for_build_wheel(self, config_settings=None): """Identify packages required for building a wheel diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index 1b5829a038a..ab2d6152890 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -5,7 +5,7 @@ distro==1.6.0 html5lib==1.1 msgpack==1.0.2 packaging==21.0 -pep517==0.11.0 +pep517==0.12.0 platformdirs==2.4.0 progress==1.6 pyparsing==2.4.7 From ae512892ebbb3bc1e479f3168c7d67b13fc48057 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Wed, 13 Oct 2021 17:43:23 +0200 Subject: [PATCH 5/7] Prepare legacy editable metadata in isolated env When there is a pyproject.toml, metadata preparation must be done in the isolated build environment for legacy editable installs too (fixes a regression). Also detect earlier if an editable install must go through the legacy install path, to be sure to run it in an environment with the correct build requirements. --- news/10573.bugfix.rst | 5 + src/pip/_internal/distributions/sdist.py | 14 ++- src/pip/_internal/req/req_install.py | 137 +++++++++-------------- src/pip/_internal/wheel_builder.py | 6 +- tests/unit/test_wheel_builder.py | 8 +- 5 files changed, 81 insertions(+), 89 deletions(-) create mode 100644 news/10573.bugfix.rst diff --git a/news/10573.bugfix.rst b/news/10573.bugfix.rst new file mode 100644 index 00000000000..5b69ab6d7e7 --- /dev/null +++ b/news/10573.bugfix.rst @@ -0,0 +1,5 @@ +When installing projects with a ``pyproject.toml`` in editable mode, and the build +backend does not support :pep:`660`, prepare metadata using +``prepare_metadata_for_build_wheel`` instead of ``setup.py egg_info``. Also, refuse +installing projects that only have a ``setup.cfg`` and no ``setup.py`` nor +``pyproject.toml``. These restore the pre-21.3 behaviour. diff --git a/src/pip/_internal/distributions/sdist.py b/src/pip/_internal/distributions/sdist.py index b4e2892931b..b8d3b604798 100644 --- a/src/pip/_internal/distributions/sdist.py +++ b/src/pip/_internal/distributions/sdist.py @@ -38,10 +38,22 @@ def prepare_distribution_metadata( def _setup_isolation(self, finder: PackageFinder) -> None: self._prepare_build_backend(finder) + # Check that if the requirement is editable, it either supports PEP 660 or has a + # setup.py or a setup.cfg. This cannot be done earlier because we need to setup + # the build backend to verify it supports build_editable, nor can it be done + # later, because we want to avoid installing build requirements needlessly. + # Doing it here also works around setuptools generating UNKNOWN.egg-info when + # running get_requires_for_build_wheel on a directory without setup.py nor + # setup.cfg. + self.req.isolated_editable_sanity_check() # Install any extra build dependencies that the backend requests. # This must be done in a second pass, as the pyproject.toml # dependencies must be installed before we can call the backend. - if self.req.editable and self.req.permit_editable_wheels: + if ( + self.req.editable + and self.req.permit_editable_wheels + and self.req.supports_pyproject_editable() + ): build_reqs = self._get_build_requires_editable() else: build_reqs = self._get_build_requires_wheel() diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index c164a351e54..19f1904af67 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -1,6 +1,7 @@ # The following comment should be removed at some point in the future. # mypy: strict-optional=False +import functools import logging import os import shutil @@ -16,7 +17,7 @@ from pip._vendor.packaging.utils import canonicalize_name from pip._vendor.packaging.version import Version from pip._vendor.packaging.version import parse as parse_version -from pip._vendor.pep517.wrappers import HookMissing, Pep517HookCaller +from pip._vendor.pep517.wrappers import Pep517HookCaller from pip._vendor.pkg_resources import Distribution from pip._internal.build_env import BuildEnvironment, NoOpBuildEnvironment @@ -53,6 +54,7 @@ redact_auth_from_url, ) from pip._internal.utils.packaging import get_metadata +from pip._internal.utils.subprocess import runner_with_spinner_message from pip._internal.utils.temp_dir import TempDirectory, tempdir_kinds from pip._internal.utils.virtualenv import running_under_virtualenv from pip._internal.vcs import vcs @@ -196,11 +198,6 @@ def __init__( # but after loading this flag should be treated as read only. self.use_pep517 = use_pep517 - # supports_pyproject_editable will be set to True or False when we try - # to prepare editable metadata or build an editable wheel. None means - # "we don't know yet". - self.supports_pyproject_editable: Optional[bool] = None - # This requirement needs more preparation before it can be built self.needs_more_preparation = False @@ -247,6 +244,18 @@ def name(self) -> Optional[str]: return None return pkg_resources.safe_name(self.req.name) + @functools.lru_cache() # use cached_property in python 3.8+ + def supports_pyproject_editable(self) -> bool: + if not self.use_pep517: + return False + assert self.pep517_backend + with self.build_env: + runner = runner_with_spinner_message( + "Checking if build backend supports build_editable" + ) + with self.pep517_backend.subprocess_runner(runner): + return "build_editable" in self.pep517_backend._supported_features() + @property def specifier(self) -> SpecifierSet: return self.req.specifier @@ -503,93 +512,59 @@ def load_pyproject_toml(self) -> None: backend_path=backend_path, ) - def _generate_editable_metadata(self) -> str: - """Invokes metadata generator functions, with the required arguments.""" - if self.use_pep517: - assert self.pep517_backend is not None - try: - metadata_directory = generate_editable_metadata( - build_env=self.build_env, - backend=self.pep517_backend, - ) - except HookMissing as e: - self.supports_pyproject_editable = False - if not os.path.exists(self.setup_py_path) and not os.path.exists( - self.setup_cfg_path - ): - raise InstallationError( - f"Project {self} has a 'pyproject.toml' and its build " - f"backend is missing the {e} hook. Since it does not " - f"have a 'setup.py' nor a 'setup.cfg', " - f"it cannot be installed in editable mode. " - f"Consider using a build backend that supports PEP 660." - ) - # At this point we have determined that the build_editable hook - # is missing, and there is a setup.py or setup.cfg - # so we fallback to the legacy metadata generation - logger.info( - "Build backend does not support editables, " - "falling back to setup.py egg_info." - ) - else: - self.supports_pyproject_editable = True - return metadata_directory - elif not os.path.exists(self.setup_py_path) and not os.path.exists( - self.setup_cfg_path - ): - raise InstallationError( - f"File 'setup.py' or 'setup.cfg' not found " - f"for legacy project {self}. " - f"It cannot be installed in editable mode." - ) - - return generate_metadata_legacy( - build_env=self.build_env, - setup_py_path=self.setup_py_path, - source_dir=self.unpacked_source_directory, - isolated=self.isolated, - details=self.name or f"from {self.link}", - ) + def isolated_editable_sanity_check(self) -> None: + """Check that an editable requirement if valid for use with PEP 517/518. - def _generate_metadata(self) -> str: - """Invokes metadata generator functions, with the required arguments.""" - if self.use_pep517: - assert self.pep517_backend is not None - try: - return generate_metadata( - build_env=self.build_env, - backend=self.pep517_backend, - ) - except HookMissing as e: - raise InstallationError( - f"Project {self} has a pyproject.toml but its build " - f"backend is missing the required {e} hook." - ) - elif not os.path.exists(self.setup_py_path): + This verifies that an editable that has a pyproject.toml either supports PEP 660 + or as a setup.py or a setup.cfg + """ + if ( + self.editable + and self.use_pep517 + and not self.supports_pyproject_editable() + and not os.path.isfile(self.setup_py_path) + and not os.path.isfile(self.setup_cfg_path) + ): raise InstallationError( - f"File 'setup.py' not found for legacy project {self}." + f"Project {self} has a 'pyproject.toml' and its build " + f"backend is missing the 'build_editable' hook. Since it does not " + f"have a 'setup.py' nor a 'setup.cfg', " + f"it cannot be installed in editable mode. " + f"Consider using a build backend that supports PEP 660." ) - return generate_metadata_legacy( - build_env=self.build_env, - setup_py_path=self.setup_py_path, - source_dir=self.unpacked_source_directory, - isolated=self.isolated, - details=self.name or f"from {self.link}", - ) - def prepare_metadata(self) -> None: """Ensure that project metadata is available. - Under PEP 517, call the backend hook to prepare the metadata. + Under PEP 517 and PEP 660, call the backend hook to prepare the metadata. Under legacy processing, call setup.py egg-info. """ assert self.source_dir - if self.editable and self.permit_editable_wheels: - self.metadata_directory = self._generate_editable_metadata() + if self.use_pep517: + assert self.pep517_backend is not None + if ( + self.editable + and self.permit_editable_wheels + and self.supports_pyproject_editable() + ): + self.metadata_directory = generate_editable_metadata( + build_env=self.build_env, + backend=self.pep517_backend, + ) + else: + self.metadata_directory = generate_metadata( + build_env=self.build_env, + backend=self.pep517_backend, + ) else: - self.metadata_directory = self._generate_metadata() + self.metadata_directory = generate_metadata_legacy( + build_env=self.build_env, + setup_py_path=self.setup_py_path, + source_dir=self.unpacked_source_directory, + isolated=self.isolated, + details=self.name or f"from {self.link}", + ) # Act on the newly generated metadata, based on the name and version. if not self.name: diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py index b4855a9a7fb..a9123a0f1f6 100644 --- a/src/pip/_internal/wheel_builder.py +++ b/src/pip/_internal/wheel_builder.py @@ -71,10 +71,8 @@ def _should_build( return False if req.editable: - if req.use_pep517 and req.supports_pyproject_editable is not False: - return True - # we don't build legacy editable requirements - return False + # we only build PEP 660 editable requirements + return req.supports_pyproject_editable() if req.use_pep517: return True diff --git a/tests/unit/test_wheel_builder.py b/tests/unit/test_wheel_builder.py index 2b8ec2fa94d..9562541ff55 100644 --- a/tests/unit/test_wheel_builder.py +++ b/tests/unit/test_wheel_builder.py @@ -39,7 +39,7 @@ def __init__( constraint: bool = False, source_dir: Optional[str] = "/tmp/pip-install-123/pendulum", use_pep517: bool = True, - supports_pyproject_editable: Optional[bool] = None, + supports_pyproject_editable: bool = False, ) -> None: self.name = name self.is_wheel = is_wheel @@ -48,7 +48,10 @@ def __init__( self.constraint = constraint self.source_dir = source_dir self.use_pep517 = use_pep517 - self.supports_pyproject_editable = supports_pyproject_editable + self._supports_pyproject_editable = supports_pyproject_editable + + def supports_pyproject_editable(self) -> bool: + return self._supports_pyproject_editable @pytest.mark.parametrize( @@ -66,7 +69,6 @@ def __init__( # We don't build reqs that are already wheels. (ReqMock(is_wheel=True), False, False), (ReqMock(editable=True, use_pep517=False), False, False), - (ReqMock(editable=True, use_pep517=True), False, True), ( ReqMock(editable=True, use_pep517=True, supports_pyproject_editable=True), False, From b07a956f4be74e0b28de4db81c86eba4dca90230 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Wed, 13 Oct 2021 15:17:45 +0200 Subject: [PATCH 6/7] Split _setup_isolation for readability --- src/pip/_internal/distributions/sdist.py | 52 ++++++++++++------------ 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/src/pip/_internal/distributions/sdist.py b/src/pip/_internal/distributions/sdist.py index b8d3b604798..cd85ac5c439 100644 --- a/src/pip/_internal/distributions/sdist.py +++ b/src/pip/_internal/distributions/sdist.py @@ -32,33 +32,22 @@ def prepare_distribution_metadata( # Set up the build isolation, if this requirement should be isolated should_isolate = self.req.use_pep517 and build_isolation if should_isolate: - self._setup_isolation(finder) + # Setup an isolated environment and install the build backend static + # requirements in it. + self._prepare_build_backend(finder) + # Check that if the requirement is editable, it either supports PEP 660 or + # has a setup.py or a setup.cfg. This cannot be done earlier because we need + # to setup the build backend to verify it supports build_editable, nor can + # it be done later, because we want to avoid installing build requirements + # needlessly. Doing it here also works around setuptools generating + # UNKNOWN.egg-info when running get_requires_for_build_wheel on a directory + # without setup.py nor setup.cfg. + self.req.isolated_editable_sanity_check() + # Install the dynamic build requirements. + self._install_build_reqs(finder) self.req.prepare_metadata() - def _setup_isolation(self, finder: PackageFinder) -> None: - self._prepare_build_backend(finder) - # Check that if the requirement is editable, it either supports PEP 660 or has a - # setup.py or a setup.cfg. This cannot be done earlier because we need to setup - # the build backend to verify it supports build_editable, nor can it be done - # later, because we want to avoid installing build requirements needlessly. - # Doing it here also works around setuptools generating UNKNOWN.egg-info when - # running get_requires_for_build_wheel on a directory without setup.py nor - # setup.cfg. - self.req.isolated_editable_sanity_check() - # Install any extra build dependencies that the backend requests. - # This must be done in a second pass, as the pyproject.toml - # dependencies must be installed before we can call the backend. - if ( - self.req.editable - and self.req.permit_editable_wheels - and self.req.supports_pyproject_editable() - ): - build_reqs = self._get_build_requires_editable() - else: - build_reqs = self._get_build_requires_wheel() - self._install_build_reqs(finder, build_reqs) - def _prepare_build_backend(self, finder: PackageFinder) -> None: # Isolate in a BuildEnvironment and install the build-time # requirements. @@ -103,8 +92,19 @@ def _get_build_requires_editable(self) -> Iterable[str]: with backend.subprocess_runner(runner): return backend.get_requires_for_build_editable() - def _install_build_reqs(self, finder: PackageFinder, reqs: Iterable[str]) -> None: - conflicting, missing = self.req.build_env.check_requirements(reqs) + def _install_build_reqs(self, finder: PackageFinder) -> None: + # Install any extra build dependencies that the backend requests. + # This must be done in a second pass, as the pyproject.toml + # dependencies must be installed before we can call the backend. + if ( + self.req.editable + and self.req.permit_editable_wheels + and self.req.supports_pyproject_editable() + ): + build_reqs = self._get_build_requires_editable() + else: + build_reqs = self._get_build_requires_wheel() + conflicting, missing = self.req.build_env.check_requirements(build_reqs) if conflicting: self._raise_conflicts("the backend dependencies", conflicting) self.req.build_env.install_requirements( From 106042f3df1bddcd8e53c2a9fab6cfaffb8e5cc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Mon, 18 Oct 2021 15:20:46 +0200 Subject: [PATCH 7/7] Simplify "Preparing metadata" logging Do not mention wheel, as this code path is also used in legacy editable mode with isolation, where no wheel is involved. This also aligns with the new log entry for setup.py egg-info which says "Preparing metadata (setup.py)". --- src/pip/_internal/operations/build/metadata.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/pip/_internal/operations/build/metadata.py b/src/pip/_internal/operations/build/metadata.py index e99af4697c9..7d12438d6ed 100644 --- a/src/pip/_internal/operations/build/metadata.py +++ b/src/pip/_internal/operations/build/metadata.py @@ -23,9 +23,7 @@ def generate_metadata(build_env: BuildEnvironment, backend: Pep517HookCaller) -> # Note that Pep517HookCaller implements a fallback for # prepare_metadata_for_build_wheel, so we don't have to # consider the possibility that this hook doesn't exist. - runner = runner_with_spinner_message( - "Preparing wheel metadata (pyproject.toml)" - ) + runner = runner_with_spinner_message("Preparing metadata (pyproject.toml)") with backend.subprocess_runner(runner): distinfo_dir = backend.prepare_metadata_for_build_wheel(metadata_dir)