From e02b1dd0219ed9f7366557a6e20267dbcf56cd44 Mon Sep 17 00:00:00 2001 From: Richard Si Date: Sat, 6 Jan 2024 19:52:25 -0500 Subject: [PATCH 1/2] Reword and migrate editable errors to rich format --- news/10421.feature.rst | 1 + src/pip/_internal/exceptions.py | 33 +++++++++++++++++++++++++++ src/pip/_internal/req/constructors.py | 9 +++----- src/pip/_internal/req/req_install.py | 14 +++++------- tests/functional/test_install.py | 3 ++- tests/functional/test_pep660.py | 26 +++++++++++++++++++-- 6 files changed, 69 insertions(+), 17 deletions(-) create mode 100644 news/10421.feature.rst diff --git a/news/10421.feature.rst b/news/10421.feature.rst new file mode 100644 index 00000000000..a5fec3312d8 --- /dev/null +++ b/news/10421.feature.rst @@ -0,0 +1 @@ +Present clearer errors when an invalid editable requirement is given or when a project's build backend does not support editable installs (PEP 660). diff --git a/src/pip/_internal/exceptions.py b/src/pip/_internal/exceptions.py index 5007a622d82..75e32fe52e9 100644 --- a/src/pip/_internal/exceptions.py +++ b/src/pip/_internal/exceptions.py @@ -726,3 +726,36 @@ def from_config( exc_info = logger.isEnabledFor(VERBOSE) logger.warning("Failed to read %s", config, exc_info=exc_info) return cls(None) + + +class InvalidEditableRequirement(DiagnosticPipError): + reference = "invalid-editable-requirement" + + def __init__(self, *, requirement: str, vcs_schemes: List[str]) -> None: + super().__init__( + message=Text(f"Cannot install {requirement} in editable mode."), + context=( + "It is not a valid editable requirement. There would be no source tree " + "that can be edited after installation." + ), + hint_stmt=( + "It should either be a path to a local project or a VCS URL " + f"(beginning with {', '.join(vcs_schemes)})." + ), + ) + + +class EditableUnsupportedByBackend(DiagnosticPipError): + reference = "editable-mode-unsupported-by-backend" + + def __init__(self, *, requirement: "InstallRequirement") -> None: + super().__init__( + message=Text(f"Cannot install {requirement} in editable mode."), + context=( + "The project has a 'pyproject.toml' and its build backend is missing " + "the 'build_editable' hook.\n" + "Since it does not have a 'setup.py' nor a 'setup.cfg', " + "it cannot be installed in editable mode. " + ), + hint_stmt=Text("Consider using a build backend that supports PEP 660."), + ) diff --git a/src/pip/_internal/req/constructors.py b/src/pip/_internal/req/constructors.py index 7e2d0e5b879..c8730916472 100644 --- a/src/pip/_internal/req/constructors.py +++ b/src/pip/_internal/req/constructors.py @@ -18,7 +18,7 @@ from pip._vendor.packaging.requirements import InvalidRequirement, Requirement from pip._vendor.packaging.specifiers import Specifier -from pip._internal.exceptions import InstallationError +from pip._internal.exceptions import InstallationError, InvalidEditableRequirement from pip._internal.models.index import PyPI, TestPyPI from pip._internal.models.link import Link from pip._internal.models.wheel import Wheel @@ -122,11 +122,8 @@ def parse_editable(editable_req: str) -> Tuple[Optional[str], str, Set[str]]: link = Link(url) if not link.is_vcs: - backends = ", ".join(vcs.all_schemes) - raise InstallationError( - f"{editable_req} is not a valid editable requirement. " - f"It should either be a path to a local project or a VCS URL " - f"(beginning with {backends})." + raise InvalidEditableRequirement( + requirement=editable_req, vcs_schemes=vcs.all_schemes ) package_name = link.egg_fragment diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index b61a219df68..ab1f4717713 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -18,7 +18,11 @@ from pip._vendor.pyproject_hooks import BuildBackendHookCaller from pip._internal.build_env import BuildEnvironment, NoOpBuildEnvironment -from pip._internal.exceptions import InstallationError, PreviousBuildDirError +from pip._internal.exceptions import ( + EditableUnsupportedByBackend, + InstallationError, + PreviousBuildDirError, +) from pip._internal.locations import get_scheme from pip._internal.metadata import ( BaseDistribution, @@ -544,13 +548,7 @@ def isolated_editable_sanity_check(self) -> None: and not os.path.isfile(self.setup_py_path) and not os.path.isfile(self.setup_cfg_path) ): - raise InstallationError( - 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." - ) + raise EditableUnsupportedByBackend(requirement=self) def prepare_metadata(self) -> None: """Ensure that project metadata is available. diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index b18fabc84c9..26ea99f037e 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -346,7 +346,8 @@ def test_basic_editable_install(script: PipTestEnvironment) -> None: Test editable installation. """ result = script.pip("install", "-e", "INITools==0.2", expect_error=True) - assert "INITools==0.2 is not a valid editable requirement" in result.stderr + assert "Cannot install INITools==0.2 in editable mode" in result.stderr + assert "It is not a valid editable requirement" in result.stderr assert not result.files_created diff --git a/tests/functional/test_pep660.py b/tests/functional/test_pep660.py index 8418b26894c..69661357ca7 100644 --- a/tests/functional/test_pep660.py +++ b/tests/functional/test_pep660.py @@ -62,11 +62,17 @@ def build_editable(wheel_directory, config_settings=None, metadata_directory=Non def _make_project( - tmpdir: Path, backend_code: str, with_setup_py: bool, with_pyproject: bool = True + tmpdir: Path, + backend_code: str, + *, + with_setup_py: bool, + with_setup_cfg: bool = True, + with_pyproject: bool = True, ) -> Path: project_dir = tmpdir / "project" project_dir.mkdir() - project_dir.joinpath("setup.cfg").write_text(SETUP_CFG) + if with_setup_cfg: + 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: @@ -223,3 +229,19 @@ def test_download_editable_pep660_basic( _assert_hook_not_called(project_dir, "prepare_metadata_for_build_editable") _assert_hook_called(project_dir, "prepare_metadata_for_build_wheel") assert len(os.listdir(str(download_dir))) == 1, "a zip should have been created" + + +def test_install_editable_unsupported_by_backend( + tmpdir: Path, script: PipTestEnvironment +) -> None: + """ + Check that pip errors out when installing a project whose backend does not + support PEP 660 and falling back to a legacy editable install is impossible + (no 'setup.py' or 'setup.py.cfg'). + """ + project_dir = _make_project( + tmpdir, BACKEND_WITHOUT_PEP660, with_setup_py=False, with_setup_cfg=False + ) + result = script.pip("install", "--editable", project_dir, expect_error=True) + assert "editable-mode-unsupported-by-backend" in result.stderr + assert "Consider using a build backend that supports PEP 660" in result.stderr From 201c116d23c07637dcabfb750c1cc3792345f8a5 Mon Sep 17 00:00:00 2001 From: Richard Si Date: Sat, 20 Jan 2024 20:14:10 -0500 Subject: [PATCH 2/2] Tweak invalid editable req error to be generic The error can raised by pip wheel (yea, I know) so it's best to avoid mentioning installation. --- src/pip/_internal/exceptions.py | 5 ++--- tests/functional/test_install.py | 3 +-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/pip/_internal/exceptions.py b/src/pip/_internal/exceptions.py index 75e32fe52e9..0a15f441716 100644 --- a/src/pip/_internal/exceptions.py +++ b/src/pip/_internal/exceptions.py @@ -733,10 +733,9 @@ class InvalidEditableRequirement(DiagnosticPipError): def __init__(self, *, requirement: str, vcs_schemes: List[str]) -> None: super().__init__( - message=Text(f"Cannot install {requirement} in editable mode."), + message=Text(f"{requirement} is not a valid editable requirement."), context=( - "It is not a valid editable requirement. There would be no source tree " - "that can be edited after installation." + "There would be no source tree that can be edited after installation." ), hint_stmt=( "It should either be a path to a local project or a VCS URL " diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 26ea99f037e..b18fabc84c9 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -346,8 +346,7 @@ def test_basic_editable_install(script: PipTestEnvironment) -> None: Test editable installation. """ result = script.pip("install", "-e", "INITools==0.2", expect_error=True) - assert "Cannot install INITools==0.2 in editable mode" in result.stderr - assert "It is not a valid editable requirement" in result.stderr + assert "INITools==0.2 is not a valid editable requirement" in result.stderr assert not result.files_created