From 5d4ed8ccedf920f57027061d1559f47cab6a9e11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Sun, 1 Aug 2021 10:35:18 +0200 Subject: [PATCH 01/14] Refactor egg_link_path/dist_is_editable Make them work without using pkg_resource. --- src/pip/_internal/utils/misc.py | 37 ++++++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index 8b1081f2928..296d711278e 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -9,6 +9,7 @@ import logging import os import posixpath +import re import shutil import stat import sys @@ -357,15 +358,23 @@ def dist_in_site_packages(dist: Distribution) -> bool: return dist_location(dist).startswith(normalize_path(site_packages)) -def dist_is_editable(dist: Distribution) -> bool: +def egg_link_path_from_sys_path(raw_name: str) -> Optional[str]: """ - Return True if given Distribution is an editable install. + Look for a .egg-link file for project name, by walking sys.path. """ + egg_link_name = _egg_link_name(raw_name) for path_item in sys.path: - egg_link = os.path.join(path_item, dist.project_name + ".egg-link") + egg_link = os.path.join(path_item, egg_link_name) if os.path.isfile(egg_link): - return True - return False + return egg_link + return None + + +def dist_is_editable(dist: Distribution) -> bool: + """ + Return True if given Distribution is an editable install. + """ + return bool(egg_link_path_from_sys_path(dist.project_name)) def get_installed_distributions( @@ -414,7 +423,16 @@ def get_distribution(req_name: str) -> Optional[Distribution]: return cast(_Dist, dist)._dist -def egg_link_path(dist: Distribution) -> Optional[str]: +def _egg_link_name(raw_name: str) -> str: + """ + Convert a Name metadata value to a .egg-link name, by applying + the same substitution as pkg_resources's safe_name function. + Note: we cannot use canonicalize_name because it has a different logic. + """ + return re.sub("[^A-Za-z0-9.]+", "-", raw_name) + ".egg-link" + + +def egg_link_path_from_location(raw_name: str) -> Optional[str]: """ Return the path for the .egg-link file if it exists, otherwise, None. @@ -442,13 +460,18 @@ def egg_link_path(dist: Distribution) -> Optional[str]: sites.append(user_site) sites.append(site_packages) + egg_link_name = _egg_link_name(raw_name) for site in sites: - egglink = os.path.join(site, dist.project_name) + ".egg-link" + egglink = os.path.join(site, egg_link_name) if os.path.isfile(egglink): return egglink return None +def egg_link_path(dist: Distribution) -> Optional[str]: + return egg_link_path_from_location(dist.project_name) + + def dist_location(dist: Distribution) -> str: """ Get the site-packages location of this distribution. Generally From 0fe818236355e40687813aa21ac9b4af8fa64778 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Sun, 1 Aug 2021 11:32:57 +0200 Subject: [PATCH 02/14] Add PEP 610 editables support to BaseDistribution Look for the editable flag in direct_url.json, and fall back to the .egg-link search if there is no direct_url.json. --- src/pip/_internal/metadata/base.py | 27 ++++++++++++++++++++- src/pip/_internal/metadata/pkg_resources.py | 4 --- src/pip/_internal/utils/misc.py | 7 ------ 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/src/pip/_internal/metadata/base.py b/src/pip/_internal/metadata/base.py index e1b229d1ff4..ef09b7f8b98 100644 --- a/src/pip/_internal/metadata/base.py +++ b/src/pip/_internal/metadata/base.py @@ -23,8 +23,11 @@ DIRECT_URL_METADATA_NAME, DirectUrl, DirectUrlValidationError, + DirInfo, ) from pip._internal.utils.misc import stdlib_pkgs # TODO: Move definition here. +from pip._internal.utils.misc import egg_link_path_from_sys_path +from pip._internal.utils.urls import url_to_path if TYPE_CHECKING: from typing import Protocol @@ -73,6 +76,28 @@ def location(self) -> Optional[str]: """ raise NotImplementedError() + @property + def editable_project_location(self) -> Optional[str]: + """The project location for editable distributions. + + This is the directory where pyproject.toml or setup.py is located. + None if the distribution is not installed in editable mode. + """ + # TODO: this property is relatively costly to compute, memoize it ? + direct_url = self.direct_url + if direct_url: + if isinstance(direct_url.info, DirInfo) and direct_url.info.editable: + return url_to_path(direct_url.url) + else: + # Search for an .egg-link file by walking sys.path, as it was + # done before by dist_is_editable(). + egg_link_path = egg_link_path_from_sys_path(self.raw_name) + if egg_link_path: + # TODO: get project location from second line of egg_link file + # (https://github.com/pypa/pip/issues/10243) + return self.location + return None + @property def info_directory(self) -> Optional[str]: """Location of the .[egg|dist]-info directory. @@ -129,7 +154,7 @@ def installer(self) -> str: @property def editable(self) -> bool: - raise NotImplementedError() + return bool(self.editable_project_location) @property def local(self) -> bool: diff --git a/src/pip/_internal/metadata/pkg_resources.py b/src/pip/_internal/metadata/pkg_resources.py index 75fd3518f2e..35e63f2c51d 100644 --- a/src/pip/_internal/metadata/pkg_resources.py +++ b/src/pip/_internal/metadata/pkg_resources.py @@ -69,10 +69,6 @@ def version(self) -> DistributionVersion: def installer(self) -> str: return get_installer(self._dist) - @property - def editable(self) -> bool: - return misc.dist_is_editable(self._dist) - @property def local(self) -> bool: return misc.dist_is_local(self._dist) diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index 296d711278e..1e253896aca 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -370,13 +370,6 @@ def egg_link_path_from_sys_path(raw_name: str) -> Optional[str]: return None -def dist_is_editable(dist: Distribution) -> bool: - """ - Return True if given Distribution is an editable install. - """ - return bool(egg_link_path_from_sys_path(dist.project_name)) - - def get_installed_distributions( local_only: bool = True, skip: Container[str] = stdlib_pkgs, From cee422f5bca9ea67a44520a47a6b9ae843efa86a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Sun, 8 Aug 2021 16:55:52 +0200 Subject: [PATCH 03/14] Refactor direct_url test helper --- tests/lib/direct_url.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/tests/lib/direct_url.py b/tests/lib/direct_url.py index 7f1dee8bd4c..ec0a32b4d66 100644 --- a/tests/lib/direct_url.py +++ b/tests/lib/direct_url.py @@ -3,15 +3,22 @@ from pip._internal.models.direct_url import DIRECT_URL_METADATA_NAME, DirectUrl from tests.lib import TestPipResult +from tests.lib.path import Path -def get_created_direct_url(result: TestPipResult, pkg: str) -> Optional[DirectUrl]: +def get_created_direct_url_path(result: TestPipResult, pkg: str) -> Optional[Path]: direct_url_metadata_re = re.compile( pkg + r"-[\d\.]+\.dist-info." + DIRECT_URL_METADATA_NAME + r"$" ) for filename in result.files_created: if direct_url_metadata_re.search(filename): - direct_url_path = result.test_env.base_path / filename - with open(direct_url_path) as f: - return DirectUrl.from_json(f.read()) + return result.test_env.base_path / filename + return None + + +def get_created_direct_url(result: TestPipResult, pkg: str) -> Optional[DirectUrl]: + direct_url_path = get_created_direct_url_path(result, pkg) + if direct_url_path: + with open(direct_url_path) as f: + return DirectUrl.from_json(f.read()) return None From d251b4bf94c195ed2fffd967fdf5cc06dbfb5ec6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Sat, 28 Aug 2021 13:18:35 +0200 Subject: [PATCH 04/14] Add editable project location to pip list output --- src/pip/_internal/commands/list.py | 16 +++++-- tests/functional/test_list.py | 77 +++++++++++++++++++++++------- 2 files changed, 72 insertions(+), 21 deletions(-) diff --git a/src/pip/_internal/commands/list.py b/src/pip/_internal/commands/list.py index abe6ef2fc80..da40bfa9b2d 100644 --- a/src/pip/_internal/commands/list.py +++ b/src/pip/_internal/commands/list.py @@ -309,12 +309,16 @@ def format_for_columns( else: header = ["Package", "Version"] - data = [] - if options.verbose >= 1 or any(x.editable for x in pkgs): + has_editables = any(x.editable for x in pkgs) + if has_editables: + header.append("Editable project location") + + if options.verbose >= 1: header.append("Location") if options.verbose >= 1: header.append("Installer") + data = [] for proj in pkgs: # if we're working on the 'outdated' list, separate out the # latest_version and type @@ -324,7 +328,10 @@ def format_for_columns( row.append(str(proj.latest_version)) row.append(proj.latest_filetype) - if options.verbose >= 1 or proj.editable: + if has_editables: + row.append(proj.editable_project_location or "") + + if options.verbose >= 1: row.append(proj.location or "") if options.verbose >= 1: row.append(proj.installer) @@ -347,5 +354,8 @@ def format_for_json(packages: "_ProcessedDists", options: Values) -> str: if options.outdated: info["latest_version"] = str(dist.latest_version) info["latest_filetype"] = dist.latest_filetype + editable_project_location = dist.editable_project_location + if editable_project_location: + info["editable_project_location"] = editable_project_location data.append(info) return json.dumps(data) diff --git a/tests/functional/test_list.py b/tests/functional/test_list.py index 5598fa9414a..80c72471d28 100644 --- a/tests/functional/test_list.py +++ b/tests/functional/test_list.py @@ -3,7 +3,9 @@ import pytest -from tests.lib import create_test_package_with_setup, wheel +from pip._internal.models.direct_url import DirectUrl +from tests.lib import _create_test_package, create_test_package_with_setup, wheel +from tests.lib.direct_url import get_created_direct_url_path from tests.lib.path import Path @@ -172,13 +174,17 @@ def test_uptodate_flag(script, data): "--uptodate", "--format=json", ) - assert {"name": "simple", "version": "1.0"} not in json.loads( - result.stdout - ) # 3.0 is latest - assert {"name": "pip-test-package", "version": "0.1.1"} in json.loads( - result.stdout - ) # editables included - assert {"name": "simple2", "version": "3.0"} in json.loads(result.stdout) + json_output = json.loads(result.stdout) + for item in json_output: + if "editable_project_location" in item: + item["editable_project_location"] = "" + assert {"name": "simple", "version": "1.0"} not in json_output # 3.0 is latest + assert { + "name": "pip-test-package", + "version": "0.1.1", + "editable_project_location": "", + } in json_output # editables included + assert {"name": "simple2", "version": "3.0"} in json_output @pytest.mark.network @@ -210,7 +216,7 @@ def test_uptodate_columns_flag(script, data): ) assert "Package" in result.stdout assert "Version" in result.stdout - assert "Location" in result.stdout # editables included + assert "Editable project location" in result.stdout # editables included assert "pip-test-package (0.1.1," not in result.stdout assert "pip-test-package 0.1.1" in result.stdout, str(result) assert "simple2 3.0" in result.stdout, str(result) @@ -244,25 +250,36 @@ def test_outdated_flag(script, data): "--outdated", "--format=json", ) + json_output = json.loads(result.stdout) + for item in json_output: + if "editable_project_location" in item: + item["editable_project_location"] = "" assert { "name": "simple", "version": "1.0", "latest_version": "3.0", "latest_filetype": "sdist", - } in json.loads(result.stdout) - assert dict( - name="simplewheel", version="1.0", latest_version="2.0", latest_filetype="wheel" - ) in json.loads(result.stdout) + } in json_output + assert ( + dict( + name="simplewheel", + version="1.0", + latest_version="2.0", + latest_filetype="wheel", + ) + in json_output + ) assert ( dict( name="pip-test-package", version="0.1", latest_version="0.1.1", latest_filetype="sdist", + editable_project_location="", ) - in json.loads(result.stdout) + in json_output ) - assert "simple2" not in {p["name"] for p in json.loads(result.stdout)} + assert "simple2" not in {p["name"] for p in json_output} @pytest.mark.network @@ -346,7 +363,7 @@ def test_editables_columns_flag(pip_test_package_script): result = pip_test_package_script.pip("list", "--editable", "--format=columns") assert "Package" in result.stdout assert "Version" in result.stdout - assert "Location" in result.stdout + assert "Editable project location" in result.stdout assert os.path.join("src", "pip-test-package") in result.stdout, str(result) @@ -384,7 +401,7 @@ def test_uptodate_editables_columns_flag(pip_test_package_script, data): ) assert "Package" in result.stdout assert "Version" in result.stdout - assert "Location" in result.stdout + assert "Editable project location" in result.stdout assert os.path.join("src", "pip-test-package") in result.stdout, str(result) @@ -433,7 +450,7 @@ def test_outdated_editables_columns_flag(script, data): ) assert "Package" in result.stdout assert "Version" in result.stdout - assert "Location" in result.stdout + assert "Editable project location" in result.stdout assert os.path.join("src", "pip-test-package") in result.stdout, str(result) @@ -684,3 +701,27 @@ def test_list_include_work_dir_pkg(script): result = script.pip("list", "--format=json", cwd=pkg_path) json_result = json.loads(result.stdout) assert {"name": "simple", "version": "1.0"} in json_result + + +def test_list_pep610_editable(script, with_wheel): + """ + Test that a package installed with a direct_url.json with editable=true + is correctly listed as editable. + """ + pkg_path = _create_test_package(script, name="testpkg") + result = script.pip("install", pkg_path) + direct_url_path = get_created_direct_url_path(result, "testpkg") + assert direct_url_path + # patch direct_url.json to simulate an editable install + with open(direct_url_path) as f: + direct_url = DirectUrl.from_json(f.read()) + direct_url.info.editable = True + with open(direct_url_path, "w") as f: + f.write(direct_url.to_json()) + result = script.pip("list", "--format=json") + for item in json.loads(result.stdout): + if item["name"] == "testpkg": + assert item["editable_project_location"] + break + else: + assert False, "package 'testpkg' not found in pip list result" From 0199c500f0fa6976e770e634550e99f9bb4f405a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Sun, 1 Aug 2021 10:48:27 +0200 Subject: [PATCH 05/14] Use editable project location in pip freeze --- src/pip/_internal/operations/freeze.py | 5 +++-- tests/functional/test_freeze.py | 21 +++++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/operations/freeze.py b/src/pip/_internal/operations/freeze.py index 518e95c8b2c..a815d5136b4 100644 --- a/src/pip/_internal/operations/freeze.py +++ b/src/pip/_internal/operations/freeze.py @@ -169,7 +169,8 @@ def _get_editable_info(dist: BaseDistribution) -> _EditableInfo: """ if not dist.editable: return _EditableInfo(requirement=None, editable=False, comments=[]) - if dist.location is None: + editable_project_location = dist.editable_project_location + if editable_project_location is None: display = _format_as_name_version(dist) logger.warning("Editable requirement not found on disk: %s", display) return _EditableInfo( @@ -178,7 +179,7 @@ def _get_editable_info(dist: BaseDistribution) -> _EditableInfo: comments=[f"# Editable install not found ({display})"], ) - location = os.path.normcase(os.path.abspath(dist.location)) + location = os.path.normcase(os.path.abspath(editable_project_location)) from pip._internal.vcs import RemoteNotFoundError, RemoteNotValidError, vcs diff --git a/tests/functional/test_freeze.py b/tests/functional/test_freeze.py index 59f35eac1d4..62df55c04cd 100644 --- a/tests/functional/test_freeze.py +++ b/tests/functional/test_freeze.py @@ -7,6 +7,7 @@ import pytest from pip._vendor.packaging.utils import canonicalize_name +from pip._internal.models.direct_url import DirectUrl from tests.lib import ( _create_test_package, _create_test_package_with_srcdir, @@ -19,6 +20,7 @@ path_to_url, wheel, ) +from tests.lib.direct_url import get_created_direct_url_path distribute_re = re.compile("^distribute==[0-9.]+\n", re.MULTILINE) @@ -975,3 +977,22 @@ def test_freeze_include_work_dir_pkg(script): # when package directory is in PYTHONPATH result = script.pip("freeze", cwd=pkg_path) assert "simple==1.0" in result.stdout + + +def test_freeze_pep610_editable(script, with_wheel): + """ + Test that a package installed with a direct_url.json with editable=true + is correctly frozeon as editable. + """ + pkg_path = _create_test_package(script, name="testpkg") + result = script.pip("install", pkg_path) + direct_url_path = get_created_direct_url_path(result, "testpkg") + assert direct_url_path + # patch direct_url.json to simulate an editable install + with open(direct_url_path) as f: + direct_url = DirectUrl.from_json(f.read()) + direct_url.info.editable = True + with open(direct_url_path, "w") as f: + f.write(direct_url.to_json()) + result = script.pip("freeze") + assert "# Editable Git install with no remote (testpkg==0.1)" in result.stdout From d051a00fc57037104fca85ad8ebf2cdbd1e32d24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Sun, 1 Aug 2021 11:11:16 +0200 Subject: [PATCH 06/14] Remove unused get_installed_distributions --- src/pip/_internal/commands/list.py | 3 +- src/pip/_internal/metadata/base.py | 2 +- src/pip/_internal/utils/misc.py | 32 +------------ tests/unit/test_utils.py | 74 +----------------------------- 4 files changed, 5 insertions(+), 106 deletions(-) diff --git a/src/pip/_internal/commands/list.py b/src/pip/_internal/commands/list.py index da40bfa9b2d..046221d81b6 100644 --- a/src/pip/_internal/commands/list.py +++ b/src/pip/_internal/commands/list.py @@ -14,7 +14,8 @@ from pip._internal.metadata import BaseDistribution, get_environment from pip._internal.models.selection_prefs import SelectionPreferences from pip._internal.network.session import PipSession -from pip._internal.utils.misc import stdlib_pkgs, tabulate, write_output +from pip._internal.utils.compat import stdlib_pkgs +from pip._internal.utils.misc import tabulate, write_output from pip._internal.utils.parallel import map_multithread if TYPE_CHECKING: diff --git a/src/pip/_internal/metadata/base.py b/src/pip/_internal/metadata/base.py index ef09b7f8b98..a03f144f99d 100644 --- a/src/pip/_internal/metadata/base.py +++ b/src/pip/_internal/metadata/base.py @@ -25,7 +25,7 @@ DirectUrlValidationError, DirInfo, ) -from pip._internal.utils.misc import stdlib_pkgs # TODO: Move definition here. +from pip._internal.utils.compat import stdlib_pkgs # TODO: Move definition here. from pip._internal.utils.misc import egg_link_path_from_sys_path from pip._internal.utils.urls import url_to_path diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index 1e253896aca..cac3f037915 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -21,7 +21,6 @@ Any, BinaryIO, Callable, - Container, ContextManager, Iterable, Iterator, @@ -40,7 +39,7 @@ from pip import __version__ from pip._internal.exceptions import CommandError from pip._internal.locations import get_major_minor_version, site_packages, user_site -from pip._internal.utils.compat import WINDOWS, stdlib_pkgs +from pip._internal.utils.compat import WINDOWS from pip._internal.utils.virtualenv import ( running_under_virtualenv, virtualenv_no_global, @@ -370,35 +369,6 @@ def egg_link_path_from_sys_path(raw_name: str) -> Optional[str]: return None -def get_installed_distributions( - local_only: bool = True, - skip: Container[str] = stdlib_pkgs, - include_editables: bool = True, - editables_only: bool = False, - user_only: bool = False, - paths: Optional[List[str]] = None, -) -> List[Distribution]: - """Return a list of installed Distribution objects. - - Left for compatibility until direct pkg_resources uses are refactored out. - """ - from pip._internal.metadata import get_default_environment, get_environment - from pip._internal.metadata.pkg_resources import Distribution as _Dist - - if paths is None: - env = get_default_environment() - else: - env = get_environment(paths) - dists = env.iter_installed_distributions( - local_only=local_only, - skip=skip, - include_editables=include_editables, - editables_only=editables_only, - user_only=user_only, - ) - return [cast(_Dist, dist)._dist for dist in dists] - - def get_distribution(req_name: str) -> Optional[Distribution]: """Given a requirement name, return the installed Distribution object. diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 30658bbf9fd..4e4c417ef03 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -31,7 +31,6 @@ egg_link_path, format_size, get_distribution, - get_installed_distributions, get_prog, hide_url, hide_value, @@ -187,9 +186,8 @@ def test_noegglink_in_sitepkgs_venv_global(self): @patch("pip._internal.utils.misc.dist_in_usersite") @patch("pip._internal.utils.misc.dist_is_local") -@patch("pip._internal.utils.misc.dist_is_editable") class TestsGetDistributions: - """Test get_installed_distributions() and get_distribution().""" + """Test get_distribution().""" class MockWorkingSet(List[Mock]): def require(self, name): @@ -219,78 +217,12 @@ def require(self, name): ) ) - def dist_is_editable(self, dist): - return dist.test_name == "editable" - def dist_is_local(self, dist): return dist.test_name != "global" and dist.test_name != "user" def dist_in_usersite(self, dist): return dist.test_name == "user" - @patch("pip._vendor.pkg_resources.working_set", workingset) - def test_editables_only( - self, mock_dist_is_editable, mock_dist_is_local, mock_dist_in_usersite - ): - mock_dist_is_editable.side_effect = self.dist_is_editable - mock_dist_is_local.side_effect = self.dist_is_local - mock_dist_in_usersite.side_effect = self.dist_in_usersite - dists = get_installed_distributions(editables_only=True) - assert len(dists) == 1, dists - assert dists[0].test_name == "editable" - - @patch("pip._vendor.pkg_resources.working_set", workingset) - def test_exclude_editables( - self, mock_dist_is_editable, mock_dist_is_local, mock_dist_in_usersite - ): - mock_dist_is_editable.side_effect = self.dist_is_editable - mock_dist_is_local.side_effect = self.dist_is_local - mock_dist_in_usersite.side_effect = self.dist_in_usersite - dists = get_installed_distributions(include_editables=False) - assert len(dists) == 1 - assert dists[0].test_name == "normal" - - @patch("pip._vendor.pkg_resources.working_set", workingset) - def test_include_globals( - self, mock_dist_is_editable, mock_dist_is_local, mock_dist_in_usersite - ): - mock_dist_is_editable.side_effect = self.dist_is_editable - mock_dist_is_local.side_effect = self.dist_is_local - mock_dist_in_usersite.side_effect = self.dist_in_usersite - dists = get_installed_distributions(local_only=False) - assert len(dists) == 4 - - @patch("pip._vendor.pkg_resources.working_set", workingset) - def test_user_only( - self, mock_dist_is_editable, mock_dist_is_local, mock_dist_in_usersite - ): - mock_dist_is_editable.side_effect = self.dist_is_editable - mock_dist_is_local.side_effect = self.dist_is_local - mock_dist_in_usersite.side_effect = self.dist_in_usersite - dists = get_installed_distributions(local_only=False, user_only=True) - assert len(dists) == 1 - assert dists[0].test_name == "user" - - @patch("pip._vendor.pkg_resources.working_set", workingset_stdlib) - def test_gte_py27_excludes( - self, mock_dist_is_editable, mock_dist_is_local, mock_dist_in_usersite - ): - mock_dist_is_editable.side_effect = self.dist_is_editable - mock_dist_is_local.side_effect = self.dist_is_local - mock_dist_in_usersite.side_effect = self.dist_in_usersite - dists = get_installed_distributions() - assert len(dists) == 0 - - @patch("pip._vendor.pkg_resources.working_set", workingset_freeze) - def test_freeze_excludes( - self, mock_dist_is_editable, mock_dist_is_local, mock_dist_in_usersite - ): - mock_dist_is_editable.side_effect = self.dist_is_editable - mock_dist_is_local.side_effect = self.dist_is_local - mock_dist_in_usersite.side_effect = self.dist_in_usersite - dists = get_installed_distributions(skip=("setuptools", "pip", "distribute")) - assert len(dists) == 0 - @pytest.mark.parametrize( "working_set, req_name", itertools.chain( @@ -306,14 +238,12 @@ def test_freeze_excludes( ) def test_get_distribution( self, - mock_dist_is_editable, mock_dist_is_local, mock_dist_in_usersite, working_set, req_name, ): """Ensure get_distribution() finds all kinds of distributions.""" - mock_dist_is_editable.side_effect = self.dist_is_editable mock_dist_is_local.side_effect = self.dist_is_local mock_dist_in_usersite.side_effect = self.dist_in_usersite with patch("pip._vendor.pkg_resources.working_set", working_set): @@ -324,11 +254,9 @@ def test_get_distribution( @patch("pip._vendor.pkg_resources.working_set", workingset) def test_get_distribution_nonexist( self, - mock_dist_is_editable, mock_dist_is_local, mock_dist_in_usersite, ): - mock_dist_is_editable.side_effect = self.dist_is_editable mock_dist_is_local.side_effect = self.dist_is_local mock_dist_in_usersite.side_effect = self.dist_in_usersite dist = get_distribution("non-exist") From 56a912a504290e417851a56c7f81a1a266c4ee20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Sun, 1 Aug 2021 12:53:49 +0200 Subject: [PATCH 07/14] Add news fragment --- news/10249.feature.rst | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 news/10249.feature.rst diff --git a/news/10249.feature.rst b/news/10249.feature.rst new file mode 100644 index 00000000000..bb0323cc3a5 --- /dev/null +++ b/news/10249.feature.rst @@ -0,0 +1,4 @@ +Support `PEP 610 `_ to detect +editable installs in ``pip freeze`` and ``pip list``. The ``pip list`` column output +has a new ``Editable project location`` column, and json output has a new +``editable_project_location`` field. From 5a38ee7ad20c114cf371624b8098c2b43c8d3917 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Sat, 21 Aug 2021 16:52:32 +0200 Subject: [PATCH 08/14] Add docs example for pip list and editable installs --- docs/html/cli/pip_list.rst | 90 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/docs/html/cli/pip_list.rst b/docs/html/cli/pip_list.rst index cb40de67d11..3e0128dab32 100644 --- a/docs/html/cli/pip_list.rst +++ b/docs/html/cli/pip_list.rst @@ -139,3 +139,93 @@ Examples docopt==0.6.2 idlex==1.13 jedi==0.9.0 + +#. List packages installed in editable mode + +When some packages are installed in editable mode, ``pip list`` outputs an +additional column that shows the directory where the editable project is +located (i.e. the directory that contains the ``pyproject.toml`` or +``setup.py`` file). + + .. tab:: Unix/macOS + + .. code-block:: console + + $ python -m pip list + Package Version Editable project location + ---------------- -------- ------------------------------------- + pip 21.2.4 + pip-test-package 0.1.1 /home/you/.venv/src/pip-test-package + setuptools 57.4.0 + wheel 0.36.2 + + + .. tab:: Windows + + .. code-block:: console + + C:\> py -m pip list + Package Version Editable project location + ---------------- -------- ---------------------------------------- + pip 21.2.4 + pip-test-package 0.1.1 C:\Users\You\.venv\src\pip-test-package + setuptools 57.4.0 + wheel 0.36.2 + +The json format outputs an additional ``editable_project_location`` field. + + .. tab:: Unix/macOS + + .. code-block:: console + + $ python -m pip list --format=json | python -m json.tool + [ + { + "name": "pip", + "version": "21.2.4", + }, + { + "name": "pip-test-package", + "version": "0.1.1", + "editable_project_location": "/home/you/.venv/src/pip-test-package" + }, + { + "name": "setuptools", + "version": "57.4.0" + }, + { + "name": "wheel", + "version": "0.36.2" + } + ] + + .. tab:: Windows + + .. code-block:: console + + C:\> py -m pip list --format=json | py -m json.tool + [ + { + "name": "pip", + "version": "21.2.4", + }, + { + "name": "pip-test-package", + "version": "0.1.1", + "editable_project_location": "C:\Users\You\.venv\src\pip-test-package" + }, + { + "name": "setuptools", + "version": "57.4.0" + }, + { + "name": "wheel", + "version": "0.36.2" + } + ] + +.. note:: + + Contrarily to the ``freeze`` comand, ``pip list --format=freeze`` will not + report editable install information, and will report the version of the + package at the time it was installed. From 729eb5d62709a7daa4c5fe6f35f19ae904aa03fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Sat, 28 Aug 2021 11:27:49 +0200 Subject: [PATCH 09/14] Improve docs Co-authored-by: Tzu-ping Chung --- docs/html/cli/pip_list.rst | 6 +++--- news/10249.feature.rst | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/html/cli/pip_list.rst b/docs/html/cli/pip_list.rst index 3e0128dab32..c84cb8c094d 100644 --- a/docs/html/cli/pip_list.rst +++ b/docs/html/cli/pip_list.rst @@ -226,6 +226,6 @@ The json format outputs an additional ``editable_project_location`` field. .. note:: - Contrarily to the ``freeze`` comand, ``pip list --format=freeze`` will not - report editable install information, and will report the version of the - package at the time it was installed. + Contrary to the ``freeze`` comand, ``pip list --format=freeze`` will not + report editable install information, but the version of the package at the + time it was installed. diff --git a/news/10249.feature.rst b/news/10249.feature.rst index bb0323cc3a5..baf8395b1af 100644 --- a/news/10249.feature.rst +++ b/news/10249.feature.rst @@ -1,4 +1,4 @@ Support `PEP 610 `_ to detect editable installs in ``pip freeze`` and ``pip list``. The ``pip list`` column output -has a new ``Editable project location`` column, and json output has a new +has a new ``Editable project location`` column, and the JSON output has a new ``editable_project_location`` field. From 1149926958ef139be136bd646ebf2808e9c0837f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Sat, 28 Aug 2021 12:12:30 +0200 Subject: [PATCH 10/14] Minor pip list refactoring --- src/pip/_internal/commands/list.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/commands/list.py b/src/pip/_internal/commands/list.py index 046221d81b6..75d8dd465ba 100644 --- a/src/pip/_internal/commands/list.py +++ b/src/pip/_internal/commands/list.py @@ -303,12 +303,11 @@ def format_for_columns( Convert the package data into something usable by output_package_listing_columns. """ + header = ["Package", "Version"] + running_outdated = options.outdated - # Adjust the header for the `pip list --outdated` case. if running_outdated: - header = ["Package", "Version", "Latest", "Type"] - else: - header = ["Package", "Version"] + header.extend(["Latest", "Type"]) has_editables = any(x.editable for x in pkgs) if has_editables: From 5fa413d363680e8122ebc9a0aceadf357b25f884 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Sat, 28 Aug 2021 12:43:20 +0200 Subject: [PATCH 11/14] Extract egg_link* function to a separate module --- src/pip/_internal/metadata/base.py | 2 +- src/pip/_internal/utils/egg_link.py | 83 +++++++++++++++++++++++++++++ src/pip/_internal/utils/misc.py | 68 +---------------------- tests/unit/test_utils.py | 2 +- 4 files changed, 87 insertions(+), 68 deletions(-) create mode 100644 src/pip/_internal/utils/egg_link.py diff --git a/src/pip/_internal/metadata/base.py b/src/pip/_internal/metadata/base.py index a03f144f99d..ba173fffa79 100644 --- a/src/pip/_internal/metadata/base.py +++ b/src/pip/_internal/metadata/base.py @@ -26,7 +26,7 @@ DirInfo, ) from pip._internal.utils.compat import stdlib_pkgs # TODO: Move definition here. -from pip._internal.utils.misc import egg_link_path_from_sys_path +from pip._internal.utils.egg_link import egg_link_path_from_sys_path from pip._internal.utils.urls import url_to_path if TYPE_CHECKING: diff --git a/src/pip/_internal/utils/egg_link.py b/src/pip/_internal/utils/egg_link.py new file mode 100644 index 00000000000..ae287671f52 --- /dev/null +++ b/src/pip/_internal/utils/egg_link.py @@ -0,0 +1,83 @@ +# The following comment should be removed at some point in the future. +# mypy: strict-optional=False + +import os +import re +import sys +from typing import Optional + +from pip._vendor.pkg_resources import Distribution + +from pip._internal.locations import site_packages, user_site +from pip._internal.utils.virtualenv import ( + running_under_virtualenv, + virtualenv_no_global, +) + +__all__ = [ + "egg_link_path_from_sys_path", + "egg_link_path_from_location", + "egg_link_path", +] + + +def _egg_link_name(raw_name: str) -> str: + """ + Convert a Name metadata value to a .egg-link name, by applying + the same substitution as pkg_resources's safe_name function. + Note: we cannot use canonicalize_name because it has a different logic. + """ + return re.sub("[^A-Za-z0-9.]+", "-", raw_name) + ".egg-link" + + +def egg_link_path_from_sys_path(raw_name: str) -> Optional[str]: + """ + Look for a .egg-link file for project name, by walking sys.path. + """ + egg_link_name = _egg_link_name(raw_name) + for path_item in sys.path: + egg_link = os.path.join(path_item, egg_link_name) + if os.path.isfile(egg_link): + return egg_link + return None + + +def egg_link_path_from_location(raw_name: str) -> Optional[str]: + """ + Return the path for the .egg-link file if it exists, otherwise, None. + + There's 3 scenarios: + 1) not in a virtualenv + try to find in site.USER_SITE, then site_packages + 2) in a no-global virtualenv + try to find in site_packages + 3) in a yes-global virtualenv + try to find in site_packages, then site.USER_SITE + (don't look in global location) + + For #1 and #3, there could be odd cases, where there's an egg-link in 2 + locations. + + This method will just return the first one found. + """ + sites = [] + if running_under_virtualenv(): + sites.append(site_packages) + if not virtualenv_no_global() and user_site: + sites.append(user_site) + else: + if user_site: + sites.append(user_site) + sites.append(site_packages) + + egg_link_name = _egg_link_name(raw_name) + for site in sites: + egglink = os.path.join(site, egg_link_name) + if os.path.isfile(egglink): + return egglink + return None + + +def egg_link_path(dist): + # type: (Distribution) -> Optional[str] + return egg_link_path_from_location(dist.project_name) diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index cac3f037915..ac4951c4c2f 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -9,7 +9,6 @@ import logging import os import posixpath -import re import shutil import stat import sys @@ -40,10 +39,8 @@ from pip._internal.exceptions import CommandError from pip._internal.locations import get_major_minor_version, site_packages, user_site from pip._internal.utils.compat import WINDOWS -from pip._internal.utils.virtualenv import ( - running_under_virtualenv, - virtualenv_no_global, -) +from pip._internal.utils.egg_link import egg_link_path +from pip._internal.utils.virtualenv import running_under_virtualenv __all__ = [ "rmtree", @@ -357,18 +354,6 @@ def dist_in_site_packages(dist: Distribution) -> bool: return dist_location(dist).startswith(normalize_path(site_packages)) -def egg_link_path_from_sys_path(raw_name: str) -> Optional[str]: - """ - Look for a .egg-link file for project name, by walking sys.path. - """ - egg_link_name = _egg_link_name(raw_name) - for path_item in sys.path: - egg_link = os.path.join(path_item, egg_link_name) - if os.path.isfile(egg_link): - return egg_link - return None - - def get_distribution(req_name: str) -> Optional[Distribution]: """Given a requirement name, return the installed Distribution object. @@ -386,55 +371,6 @@ def get_distribution(req_name: str) -> Optional[Distribution]: return cast(_Dist, dist)._dist -def _egg_link_name(raw_name: str) -> str: - """ - Convert a Name metadata value to a .egg-link name, by applying - the same substitution as pkg_resources's safe_name function. - Note: we cannot use canonicalize_name because it has a different logic. - """ - return re.sub("[^A-Za-z0-9.]+", "-", raw_name) + ".egg-link" - - -def egg_link_path_from_location(raw_name: str) -> Optional[str]: - """ - Return the path for the .egg-link file if it exists, otherwise, None. - - There's 3 scenarios: - 1) not in a virtualenv - try to find in site.USER_SITE, then site_packages - 2) in a no-global virtualenv - try to find in site_packages - 3) in a yes-global virtualenv - try to find in site_packages, then site.USER_SITE - (don't look in global location) - - For #1 and #3, there could be odd cases, where there's an egg-link in 2 - locations. - - This method will just return the first one found. - """ - sites = [] - if running_under_virtualenv(): - sites.append(site_packages) - if not virtualenv_no_global() and user_site: - sites.append(user_site) - else: - if user_site: - sites.append(user_site) - sites.append(site_packages) - - egg_link_name = _egg_link_name(raw_name) - for site in sites: - egglink = os.path.join(site, egg_link_name) - if os.path.isfile(egglink): - return egglink - return None - - -def egg_link_path(dist: Distribution) -> Optional[str]: - return egg_link_path_from_location(dist.project_name) - - def dist_location(dist: Distribution) -> str: """ Get the site-packages location of this distribution. Generally diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 4e4c417ef03..4bbf4f9d396 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -67,7 +67,7 @@ def setup(self): ) # patches - from pip._internal.utils import misc as utils + from pip._internal.utils import egg_link as utils self.old_site_packages = utils.site_packages self.mock_site_packages = utils.site_packages = "SITE_PACKAGES" From 2de1e5b1bfa2004cbbe01c0dedbc7c5f20df0feb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Sat, 28 Aug 2021 12:54:13 +0200 Subject: [PATCH 12/14] Remove egg_link_path() --- src/pip/_internal/req/req_uninstall.py | 4 +- src/pip/_internal/utils/egg_link.py | 8 ---- src/pip/_internal/utils/misc.py | 4 +- tests/unit/test_utils.py | 52 +++++++++++++++++++------- 4 files changed, 42 insertions(+), 26 deletions(-) diff --git a/src/pip/_internal/req/req_uninstall.py b/src/pip/_internal/req/req_uninstall.py index ef7352f7ba3..779e93b44af 100644 --- a/src/pip/_internal/req/req_uninstall.py +++ b/src/pip/_internal/req/req_uninstall.py @@ -12,12 +12,12 @@ from pip._internal.exceptions import UninstallationError from pip._internal.locations import get_bin_prefix, get_bin_user from pip._internal.utils.compat import WINDOWS +from pip._internal.utils.egg_link import egg_link_path_from_location from pip._internal.utils.logging import getLogger, indent_log from pip._internal.utils.misc import ( ask, dist_in_usersite, dist_is_local, - egg_link_path, is_local, normalize_path, renames, @@ -459,7 +459,7 @@ def from_dist(cls, dist: Distribution) -> "UninstallPathSet": return cls(dist) paths_to_remove = cls(dist) - develop_egg_link = egg_link_path(dist) + develop_egg_link = egg_link_path_from_location(dist.project_name) develop_egg_link_egg_info = "{}.egg-info".format( pkg_resources.to_filename(dist.project_name) ) diff --git a/src/pip/_internal/utils/egg_link.py b/src/pip/_internal/utils/egg_link.py index ae287671f52..9e0da8d2d29 100644 --- a/src/pip/_internal/utils/egg_link.py +++ b/src/pip/_internal/utils/egg_link.py @@ -6,8 +6,6 @@ import sys from typing import Optional -from pip._vendor.pkg_resources import Distribution - from pip._internal.locations import site_packages, user_site from pip._internal.utils.virtualenv import ( running_under_virtualenv, @@ -17,7 +15,6 @@ __all__ = [ "egg_link_path_from_sys_path", "egg_link_path_from_location", - "egg_link_path", ] @@ -76,8 +73,3 @@ def egg_link_path_from_location(raw_name: str) -> Optional[str]: if os.path.isfile(egglink): return egglink return None - - -def egg_link_path(dist): - # type: (Distribution) -> Optional[str] - return egg_link_path_from_location(dist.project_name) diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index ac4951c4c2f..d3e9053efd7 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -39,7 +39,7 @@ from pip._internal.exceptions import CommandError from pip._internal.locations import get_major_minor_version, site_packages, user_site from pip._internal.utils.compat import WINDOWS -from pip._internal.utils.egg_link import egg_link_path +from pip._internal.utils.egg_link import egg_link_path_from_location from pip._internal.utils.virtualenv import running_under_virtualenv __all__ = [ @@ -380,7 +380,7 @@ def dist_location(dist: Distribution) -> str: The returned location is normalized (in particular, with symlinks removed). """ - egg_link = egg_link_path(dist) + egg_link = egg_link_path_from_location(dist.project_name) if egg_link: return normalize_path(egg_link) return normalize_path(dist.location) diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 4bbf4f9d396..182a13ea0ed 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -17,6 +17,7 @@ from pip._internal.exceptions import HashMismatch, HashMissing, InstallationError from pip._internal.utils.deprecation import PipDeprecationWarning, deprecated +from pip._internal.utils.egg_link import egg_link_path_from_location from pip._internal.utils.encoding import BOMS, auto_decode from pip._internal.utils.glibc import ( glibc_version_string, @@ -28,7 +29,6 @@ HiddenText, build_netloc, build_url_from_netloc, - egg_link_path, format_size, get_distribution, get_prog, @@ -51,7 +51,7 @@ class Tests_EgglinkPath: - "util.egg_link_path() tests" + "util.egg_link_path_from_location() tests" def setup(self): @@ -106,19 +106,25 @@ def test_egglink_in_usersite_notvenv(self): self.mock_virtualenv_no_global.return_value = False self.mock_running_under_virtualenv.return_value = False self.mock_isfile.side_effect = self.eggLinkInUserSite - assert egg_link_path(self.mock_dist) == self.user_site_egglink + assert ( + egg_link_path_from_location(self.mock_dist.project_name) + == self.user_site_egglink + ) def test_egglink_in_usersite_venv_noglobal(self): self.mock_virtualenv_no_global.return_value = True self.mock_running_under_virtualenv.return_value = True self.mock_isfile.side_effect = self.eggLinkInUserSite - assert egg_link_path(self.mock_dist) is None + assert egg_link_path_from_location(self.mock_dist.project_name) is None def test_egglink_in_usersite_venv_global(self): self.mock_virtualenv_no_global.return_value = False self.mock_running_under_virtualenv.return_value = True self.mock_isfile.side_effect = self.eggLinkInUserSite - assert egg_link_path(self.mock_dist) == self.user_site_egglink + assert ( + egg_link_path_from_location(self.mock_dist.project_name) + == self.user_site_egglink + ) # ####################### # # # egglink in sitepkgs # # @@ -127,19 +133,28 @@ def test_egglink_in_sitepkgs_notvenv(self): self.mock_virtualenv_no_global.return_value = False self.mock_running_under_virtualenv.return_value = False self.mock_isfile.side_effect = self.eggLinkInSitePackages - assert egg_link_path(self.mock_dist) == self.site_packages_egglink + assert ( + egg_link_path_from_location(self.mock_dist.project_name) + == self.site_packages_egglink + ) def test_egglink_in_sitepkgs_venv_noglobal(self): self.mock_virtualenv_no_global.return_value = True self.mock_running_under_virtualenv.return_value = True self.mock_isfile.side_effect = self.eggLinkInSitePackages - assert egg_link_path(self.mock_dist) == self.site_packages_egglink + assert ( + egg_link_path_from_location(self.mock_dist.project_name) + == self.site_packages_egglink + ) def test_egglink_in_sitepkgs_venv_global(self): self.mock_virtualenv_no_global.return_value = False self.mock_running_under_virtualenv.return_value = True self.mock_isfile.side_effect = self.eggLinkInSitePackages - assert egg_link_path(self.mock_dist) == self.site_packages_egglink + assert ( + egg_link_path_from_location(self.mock_dist.project_name) + == self.site_packages_egglink + ) # ################################## # # # egglink in usersite & sitepkgs # # @@ -148,19 +163,28 @@ def test_egglink_in_both_notvenv(self): self.mock_virtualenv_no_global.return_value = False self.mock_running_under_virtualenv.return_value = False self.mock_isfile.return_value = True - assert egg_link_path(self.mock_dist) == self.user_site_egglink + assert ( + egg_link_path_from_location(self.mock_dist.project_name) + == self.user_site_egglink + ) def test_egglink_in_both_venv_noglobal(self): self.mock_virtualenv_no_global.return_value = True self.mock_running_under_virtualenv.return_value = True self.mock_isfile.return_value = True - assert egg_link_path(self.mock_dist) == self.site_packages_egglink + assert ( + egg_link_path_from_location(self.mock_dist.project_name) + == self.site_packages_egglink + ) def test_egglink_in_both_venv_global(self): self.mock_virtualenv_no_global.return_value = False self.mock_running_under_virtualenv.return_value = True self.mock_isfile.return_value = True - assert egg_link_path(self.mock_dist) == self.site_packages_egglink + assert ( + egg_link_path_from_location(self.mock_dist.project_name) + == self.site_packages_egglink + ) # ############## # # # no egglink # # @@ -169,19 +193,19 @@ def test_noegglink_in_sitepkgs_notvenv(self): self.mock_virtualenv_no_global.return_value = False self.mock_running_under_virtualenv.return_value = False self.mock_isfile.return_value = False - assert egg_link_path(self.mock_dist) is None + assert egg_link_path_from_location(self.mock_dist.project_name) is None def test_noegglink_in_sitepkgs_venv_noglobal(self): self.mock_virtualenv_no_global.return_value = True self.mock_running_under_virtualenv.return_value = True self.mock_isfile.return_value = False - assert egg_link_path(self.mock_dist) is None + assert egg_link_path_from_location(self.mock_dist.project_name) is None def test_noegglink_in_sitepkgs_venv_global(self): self.mock_virtualenv_no_global.return_value = False self.mock_running_under_virtualenv.return_value = True self.mock_isfile.return_value = False - assert egg_link_path(self.mock_dist) is None + assert egg_link_path_from_location(self.mock_dist.project_name) is None @patch("pip._internal.utils.misc.dist_in_usersite") From aa06366657bf5ebf183205fe14ce7345a77f601a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Sat, 28 Aug 2021 15:50:58 +0200 Subject: [PATCH 13/14] Remove dead code I have tried to remove the .egg-link (in which case the package is not considered editable), and removing the package while leaving the .egg-link (in which case the package is not shown). I could not produce this "Editable install not found" message with pip 21.2. So I think this is dead code and I'm replacing this test by an assert. --- src/pip/_internal/operations/freeze.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/pip/_internal/operations/freeze.py b/src/pip/_internal/operations/freeze.py index a815d5136b4..be484f47db8 100644 --- a/src/pip/_internal/operations/freeze.py +++ b/src/pip/_internal/operations/freeze.py @@ -170,15 +170,7 @@ def _get_editable_info(dist: BaseDistribution) -> _EditableInfo: if not dist.editable: return _EditableInfo(requirement=None, editable=False, comments=[]) editable_project_location = dist.editable_project_location - if editable_project_location is None: - display = _format_as_name_version(dist) - logger.warning("Editable requirement not found on disk: %s", display) - return _EditableInfo( - requirement=None, - editable=True, - comments=[f"# Editable install not found ({display})"], - ) - + assert editable_project_location location = os.path.normcase(os.path.abspath(editable_project_location)) from pip._internal.vcs import RemoteNotFoundError, RemoteNotValidError, vcs From ca0517621d61ebcb557f3daf7fab85fb9c7bdfc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Mon, 30 Aug 2021 08:46:53 +0200 Subject: [PATCH 14/14] Refactor direct url editable test --- src/pip/_internal/metadata/base.py | 3 +-- src/pip/_internal/models/direct_url.py | 3 +++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/metadata/base.py b/src/pip/_internal/metadata/base.py index ba173fffa79..7eacd00e287 100644 --- a/src/pip/_internal/metadata/base.py +++ b/src/pip/_internal/metadata/base.py @@ -23,7 +23,6 @@ DIRECT_URL_METADATA_NAME, DirectUrl, DirectUrlValidationError, - DirInfo, ) from pip._internal.utils.compat import stdlib_pkgs # TODO: Move definition here. from pip._internal.utils.egg_link import egg_link_path_from_sys_path @@ -86,7 +85,7 @@ def editable_project_location(self) -> Optional[str]: # TODO: this property is relatively costly to compute, memoize it ? direct_url = self.direct_url if direct_url: - if isinstance(direct_url.info, DirInfo) and direct_url.info.editable: + if direct_url.is_local_editable(): return url_to_path(direct_url.url) else: # Search for an .egg-link file by walking sys.path, as it was diff --git a/src/pip/_internal/models/direct_url.py b/src/pip/_internal/models/direct_url.py index d652ce15356..92060d45db8 100644 --- a/src/pip/_internal/models/direct_url.py +++ b/src/pip/_internal/models/direct_url.py @@ -215,3 +215,6 @@ def from_json(cls, s: str) -> "DirectUrl": def to_json(self) -> str: return json.dumps(self.to_dict(), sort_keys=True) + + def is_local_editable(self) -> bool: + return isinstance(self.info, DirInfo) and self.info.editable