From 4a4b613a7ccdf4c4aab8f223f9b97f413b8b3056 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung Date: Thu, 21 Oct 2021 12:01:45 +0800 Subject: [PATCH] Merge PR #10577 from sbidoul/fix-pep660-metadata-preparation-fallback --- news/10531.bugfix.rst | 2 + news/10573.bugfix.rst | 5 + news/pep517.vendor.rst | 1 + src/pip/_internal/distributions/sdist.py | 40 +++-- .../_internal/operations/build/metadata.py | 4 +- src/pip/_internal/operations/prepare.py | 1 + src/pip/_internal/pyproject.py | 6 + src/pip/_internal/req/constructors.py | 2 + src/pip/_internal/req/req_install.py | 137 +++++++----------- src/pip/_internal/wheel_builder.py | 6 +- 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 +- .../_vendor/pep517/in_process/_in_process.py | 14 ++ src/pip/_vendor/pep517/wrappers.py | 4 + src/pip/_vendor/vendor.txt | 2 +- .../data/src/pep517_setup_cfg_only/setup.cfg | 3 + tests/functional/test_install.py | 30 +++- tests/functional/test_pep660.py | 13 +- tests/unit/test_pep517.py | 16 ++ tests/unit/test_wheel_builder.py | 8 +- 23 files changed, 193 insertions(+), 120 deletions(-) create mode 100644 news/10531.bugfix.rst create mode 100644 news/10573.bugfix.rst create mode 100644 news/pep517.vendor.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/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/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/_internal/distributions/sdist.py b/src/pip/_internal/distributions/sdist.py index b4e2892931b..cd85ac5c439 100644 --- a/src/pip/_internal/distributions/sdist.py +++ b/src/pip/_internal/distributions/sdist.py @@ -32,21 +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) - # 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: - 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. @@ -91,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( 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) 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" 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/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index ff0dd2f2d58..95dacab53ef 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/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 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..ac96b1f940e 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): @@ -689,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 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"), [ 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,