diff --git a/news/5349.bugfix.rst b/news/5349.bugfix.rst new file mode 100644 index 0000000000..67a5739794 --- /dev/null +++ b/news/5349.bugfix.rst @@ -0,0 +1 @@ +Modernize ``pipenv`` path patch with ``importlib.util`` to eliminate import of ``pkg_resources`` diff --git a/pipenv/core.py b/pipenv/core.py index 5294512cd8..8cbbab0ba0 100644 --- a/pipenv/core.py +++ b/pipenv/core.py @@ -968,7 +968,6 @@ def do_create_virtualenv(project, python=None, site_packages=None, pypi_mirror=N pipfile=project.parsed_pipfile, project=project, ) - project._environment.add_dist("pipenv") # Say where the virtualenv is. do_where(project, virtualenv=True, bare=False) diff --git a/pipenv/environment.py b/pipenv/environment.py index 83a53ef9e7..fec816e4dd 100644 --- a/pipenv/environment.py +++ b/pipenv/environment.py @@ -2,6 +2,7 @@ import contextlib import importlib +import importlib.util import itertools import json import operator @@ -11,12 +12,10 @@ from pathlib import Path from sysconfig import get_paths, get_python_version, get_scheme_names -import pkg_resources - -import pipenv from pipenv.patched.pip._internal.commands.install import InstallCommand from pipenv.patched.pip._internal.index.package_finder import PackageFinder from pipenv.patched.pip._internal.req.req_uninstall import UninstallPathSet +from pipenv.patched.pip._vendor import pkg_resources from pipenv.patched.pip._vendor.packaging.utils import canonicalize_name from pipenv.utils.constants import is_type_checking from pipenv.utils.indexes import prepare_pip_source_args @@ -33,15 +32,13 @@ if is_type_checking(): - from types import ModuleType - from typing import ContextManager, Dict, Generator, List, Optional, Set, Union + from typing import ContextManager, Dict, Generator, List, Optional, Union import tomlkit from pipenv.project import Project, TPipfile, TSource BASE_WORKING_SET = pkg_resources.WorkingSet(sys.path) -# TODO: Unittests for this class class Environment: @@ -56,7 +53,6 @@ def __init__( project: Optional[Project] = None, ): super().__init__() - self._modules = {"pkg_resources": pkg_resources, "pipenv": pipenv} self.base_working_set = base_working_set if base_working_set else BASE_WORKING_SET prefix = normalize_path(prefix) self._python = None @@ -72,7 +68,6 @@ def __init__( if project and not pipfile: pipfile = project.parsed_pipfile self.pipfile = pipfile - self.extra_dists = [] prefix = prefix if prefix else sys.prefix self.prefix = Path(prefix) self._base_paths = {} @@ -80,67 +75,10 @@ def __init__( self._base_paths = self.get_paths() self.sys_paths = get_paths() - def safe_import(self, name: str) -> ModuleType: - """Helper utility for reimporting previously imported modules while inside the env""" - module = None - if name not in self._modules: - self._modules[name] = importlib.import_module(name) - module = self._modules[name] - if not module: - dist = next( - iter(dist for dist in self.base_working_set if dist.project_name == name), - None, - ) - if dist: - dist.activate() - module = importlib.import_module(name) - return module - - @classmethod - def resolve_dist( - cls, dist: pkg_resources.Distribution, working_set: pkg_resources.WorkingSet - ) -> Set[pkg_resources.Distribution]: - """Given a local distribution and a working set, returns all dependencies from the set. - - :param dist: A single distribution to find the dependencies of - :type dist: :class:`pkg_resources.Distribution` - :param working_set: A working set to search for all packages - :type working_set: :class:`pkg_resources.WorkingSet` - :return: A set of distributions which the package depends on, including the package - :rtype: set(:class:`pkg_resources.Distribution`) - """ - - deps = set() - deps.add(dist) - try: - reqs = dist.requires() - # KeyError = limited metadata can be found - except (KeyError, AttributeError, OSError): # The METADATA file can't be found - return deps - for req in reqs: - try: - dist = working_set.find(req) - except pkg_resources.VersionConflict: - # https://github.com/pypa/pipenv/issues/4549 - # The requirement is already present with incompatible version. - continue - deps |= cls.resolve_dist(dist, working_set) - return deps - - def extend_dists(self, dist: pkg_resources.Distribution) -> None: - extras = self.resolve_dist(dist, self.base_working_set) - self.extra_dists.append(dist) - if extras: - self.extra_dists.extend(extras) - - def add_dist(self, dist_name: str) -> None: - dist = pkg_resources.get_distribution(pkg_resources.Requirement(dist_name)) - self.extend_dists(dist) - @cached_property def python_version(self) -> str: with self.activated(): - sysconfig = self.safe_import("sysconfig") + sysconfig = importlib.import_module("sysconfig") py_version = sysconfig.get_python_version() return py_version @@ -547,7 +485,6 @@ def get_distributions(self) -> Generator[pkg_resources.Distribution, None, None] :return: A set of distributions found on the library path :rtype: iterator """ - pip_target_dir = os.environ.get("PIP_TARGET") libdirs = ( [pip_target_dir] @@ -863,7 +800,7 @@ def run_activate_this(self): exec(code, dict(__file__=activate_this)) @contextlib.contextmanager - def activated(self, include_extras=True, extra_dists=None): + def activated(self): """Helper context manager to activate the environment. This context manager will set the following variables for the duration @@ -882,16 +819,8 @@ def activated(self, include_extras=True, extra_dists=None): to `os.environ["PATH"]` to ensure that calls to `~Environment.run()` use the environment's path preferentially. """ - - if not extra_dists: - extra_dists = [] original_path = sys.path original_prefix = sys.prefix - parent_path = Path(__file__).absolute().parent - vendor_dir = parent_path.joinpath("vendor").as_posix() - patched_dir = parent_path.joinpath("patched").as_posix() - parent_path = parent_path.as_posix() - self.add_dist("pip") prefix = self.prefix.as_posix() with vistir.contextmanagers.temp_environ(), vistir.contextmanagers.temp_path(): os.environ["PATH"] = os.pathsep.join( @@ -914,21 +843,6 @@ def activated(self, include_extras=True, extra_dists=None): os.environ.pop("PYTHONHOME", None) sys.path = self.sys_path sys.prefix = self.sys_prefix - site.addsitedir(self.base_paths["purelib"]) - pip = self.safe_import("pip") # noqa - pip_vendor = self.safe_import("pip._vendor") - pep517_dir = os.path.join(os.path.dirname(pip_vendor.__file__), "pep517") - site.addsitedir(pep517_dir) - os.environ["PYTHONPATH"] = os.pathsep.join( - [os.environ.get("PYTHONPATH", self.base_paths["PYTHONPATH"]), pep517_dir] - ) - if include_extras: - site.addsitedir(parent_path) - sys.path.extend([parent_path, patched_dir, vendor_dir]) - extra_dists = list(self.extra_dists) + extra_dists - for extra_dist in extra_dists: - if extra_dist not in self.get_working_set(): - extra_dist.activate(self.sys_path) try: yield finally: diff --git a/pipenv/resolver.py b/pipenv/resolver.py index cfda8373b4..3fea0a1e7a 100644 --- a/pipenv/resolver.py +++ b/pipenv/resolver.py @@ -1,3 +1,4 @@ +import importlib.util import json import logging import os @@ -6,65 +7,13 @@ os.environ["PIP_PYTHON_PATH"] = str(sys.executable) -def find_site_path(pkg, site_dir=None): - import pkg_resources - - if site_dir is None: - site_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - working_set = pkg_resources.WorkingSet([site_dir] + sys.path[:]) - for dist in working_set: - root = dist.location - base_name = dist.project_name if dist.project_name else dist.key - name = None - if "top_level.txt" in dist.metadata_listdir(""): - name = next( - iter( - [ - line.strip() - for line in dist.get_metadata_lines("top_level.txt") - if line is not None - ] - ), - None, - ) - if name is None: - name = pkg_resources.safe_name(base_name).replace("-", "_") - if not any(pkg == _ for _ in [base_name, name]): - continue - path_options = [name, f"{name}.py"] - path_options = [os.path.join(root, p) for p in path_options if p is not None] - path = next(iter(p for p in path_options if os.path.exists(p)), None) - if path is not None: - return dist, path - return None, None - - -def _patch_path(pipenv_site=None): - import site - - pipenv_libdir = os.path.dirname(os.path.abspath(__file__)) - pipenv_site_dir = os.path.dirname(pipenv_libdir) - if pipenv_site is not None: - pipenv_dist, pipenv_path = find_site_path("pipenv", site_dir=pipenv_site) - else: - pipenv_dist, pipenv_path = find_site_path("pipenv", site_dir=pipenv_site_dir) - if pipenv_dist is not None: - pipenv_dist.activate() - else: - site.addsitedir( - next( - iter( - sitedir - for sitedir in (pipenv_site, pipenv_site_dir) - if sitedir is not None - ), - None, - ) - ) - if pipenv_path is not None: - pipenv_libdir = pipenv_path - for _dir in ("vendor", "patched", pipenv_libdir): - sys.path.insert(0, os.path.join(pipenv_libdir, _dir)) +def _ensure_modules(): + spec = importlib.util.spec_from_file_location( + "pipenv", location=os.path.join(os.path.dirname(__file__), "__init__.py") + ) + pipenv = importlib.util.module_from_spec(spec) + sys.modules["pipenv"] = pipenv + spec.loader.exec_module(pipenv) def get_parser(): @@ -838,7 +787,7 @@ def _main( def main(argv=None): parser = get_parser() parsed, remaining = parser.parse_known_args(argv) - _patch_path(pipenv_site=parsed.pipenv_site) + _ensure_modules() import warnings from pipenv.vendor.vistir.misc import replace_with_text_stream