diff --git a/docs/html/cli/pip_list.rst b/docs/html/cli/pip_list.rst
index cb40de67d11..c84cb8c094d 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::
+
+ 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
new file mode 100644
index 00000000000..baf8395b1af
--- /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 the JSON output has a new
+``editable_project_location`` field.
diff --git a/src/pip/_internal/commands/list.py b/src/pip/_internal/commands/list.py
index abe6ef2fc80..75d8dd465ba 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:
@@ -302,19 +303,22 @@ 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"])
- 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/src/pip/_internal/metadata/base.py b/src/pip/_internal/metadata/base.py
index e1b229d1ff4..7eacd00e287 100644
--- a/src/pip/_internal/metadata/base.py
+++ b/src/pip/_internal/metadata/base.py
@@ -24,7 +24,9 @@
DirectUrl,
DirectUrlValidationError,
)
-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.egg_link 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 +75,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 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
+ # 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 +153,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/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
diff --git a/src/pip/_internal/operations/freeze.py b/src/pip/_internal/operations/freeze.py
index 518e95c8b2c..be484f47db8 100644
--- a/src/pip/_internal/operations/freeze.py
+++ b/src/pip/_internal/operations/freeze.py
@@ -169,16 +169,9 @@ def _get_editable_info(dist: BaseDistribution) -> _EditableInfo:
"""
if not dist.editable:
return _EditableInfo(requirement=None, editable=False, comments=[])
- if dist.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})"],
- )
-
- location = os.path.normcase(os.path.abspath(dist.location))
+ editable_project_location = dist.editable_project_location
+ assert editable_project_location
+ location = os.path.normcase(os.path.abspath(editable_project_location))
from pip._internal.vcs import RemoteNotFoundError, RemoteNotValidError, vcs
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
new file mode 100644
index 00000000000..9e0da8d2d29
--- /dev/null
+++ b/src/pip/_internal/utils/egg_link.py
@@ -0,0 +1,75 @@
+# 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._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",
+]
+
+
+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
diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py
index 8b1081f2928..d3e9053efd7 100644
--- a/src/pip/_internal/utils/misc.py
+++ b/src/pip/_internal/utils/misc.py
@@ -20,7 +20,6 @@
Any,
BinaryIO,
Callable,
- Container,
ContextManager,
Iterable,
Iterator,
@@ -39,11 +38,9 @@
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.virtualenv import (
- running_under_virtualenv,
- virtualenv_no_global,
-)
+from pip._internal.utils.compat import WINDOWS
+from pip._internal.utils.egg_link import egg_link_path_from_location
+from pip._internal.utils.virtualenv import running_under_virtualenv
__all__ = [
"rmtree",
@@ -357,46 +354,6 @@ def dist_in_site_packages(dist: Distribution) -> bool:
return dist_location(dist).startswith(normalize_path(site_packages))
-def dist_is_editable(dist: Distribution) -> bool:
- """
- Return True if given Distribution is an editable install.
- """
- for path_item in sys.path:
- egg_link = os.path.join(path_item, dist.project_name + ".egg-link")
- if os.path.isfile(egg_link):
- return True
- return False
-
-
-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.
@@ -414,41 +371,6 @@ def get_distribution(req_name: str) -> Optional[Distribution]:
return cast(_Dist, dist)._dist
-def egg_link_path(dist: Distribution) -> 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)
-
- for site in sites:
- egglink = os.path.join(site, dist.project_name) + ".egg-link"
- if os.path.isfile(egglink):
- return egglink
- return None
-
-
def dist_location(dist: Distribution) -> str:
"""
Get the site-packages location of this distribution. Generally
@@ -458,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/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
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"
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
diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py
index 30658bbf9fd..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,10 +29,8 @@
HiddenText,
build_netloc,
build_url_from_netloc,
- egg_link_path,
format_size,
get_distribution,
- get_installed_distributions,
get_prog,
hide_url,
hide_value,
@@ -52,7 +51,7 @@
class Tests_EgglinkPath:
- "util.egg_link_path() tests"
+ "util.egg_link_path_from_location() tests"
def setup(self):
@@ -68,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"
@@ -107,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 # #
@@ -128,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 # #
@@ -149,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 # #
@@ -170,26 +193,25 @@ 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")
@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 +241,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 +262,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 +278,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")