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")