Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Begin converting away from pkg_resources #5351

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions news/5349.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Modernize ``pipenv`` path patch with ``importlib.util`` to eliminate import of ``pkg_resources``
1 change: 0 additions & 1 deletion pipenv/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
96 changes: 5 additions & 91 deletions pipenv/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import contextlib
import importlib
import importlib.util
import itertools
import json
import operator
Expand All @@ -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
Expand All @@ -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:
Expand All @@ -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
Expand All @@ -72,75 +68,17 @@ 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 = {}
if self.is_venv:
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

Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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
Expand All @@ -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(
Expand All @@ -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:
Expand Down
69 changes: 9 additions & 60 deletions pipenv/resolver.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import importlib.util
import json
import logging
import os
Expand All @@ -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():
Expand Down Expand Up @@ -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
Expand Down