check point progress on only bringing in pip==22.0.4 (#4966)

* vendor in pip==22.0.4

* updating vendor packaging version

* update pipdeptree to fix pipenv graph with new version of pip.

* Vendoring of pip-shims 0.7.0

* Vendoring of requirementslib 1.6.3

* Update pip index safety restrictions patch for pip==22.0.4

* Update patches

* exclude pyptoject.toml from black to see if that helps.

* Move this part of the hash collection back to the top (like prior implementation) because it affects the outcome of this test now in pip 22.0.4
This commit is contained in:
Matt Davis
2022-04-18 23:48:38 -04:00
committed by GitHub
parent 6034244498
commit f3166e673f
398 changed files with 64720 additions and 26513 deletions
+1 -1
View File
@@ -74,7 +74,7 @@ jobs:
PYTHONIOENCODING: "utf-8"
GIT_ASK_YESNO: "false"
run: |
pipenv run pre-commit run --all-files --verbose
pipenv run pre-commit run --all-files --verbose --show-diff-on-failure
- name: Run tests
env:
PIPENV_DEFAULT_PYTHON_VERSION: ${{ matrix.python-version }}
Generated
+9 -9
View File
@@ -462,11 +462,11 @@
},
"platformdirs": {
"hashes": [
"sha256:7535e70dfa32e84d4b34996ea99c5e432fa29a708d0f4e394bbcb2a8faa4f16d",
"sha256:bcae7cab893c2d310a711b70b24efb93334febe65f8de776ee320b517471e227"
"sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788",
"sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"
],
"markers": "python_version >= '3.7'",
"version": "==2.5.1"
"version": "==2.5.2"
},
"pluggy": {
"hashes": [
@@ -668,7 +668,7 @@
"sha256:c50f3d253bc6a9bb9c79d61a26d510d74abdf1b16881260fab5edfc3edfb082f",
"sha256:ea74bc9dad9589d8eea3e3fd0b136d8bf6e428888955f215824c2894f0da8b47"
],
"markers": "python_version < '4' and python_full_version >= '3.6.3'",
"markers": "python_full_version >= '3.6.3' and python_full_version < '4.0.0'",
"version": "==12.2.0"
},
"setuptools": {
@@ -794,7 +794,7 @@
"sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc",
"sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"
],
"markers": "python_version >= '3.6'",
"markers": "python_version >= '3.7'",
"version": "==2.0.1"
},
"towncrier": {
@@ -814,18 +814,18 @@
},
"typing-extensions": {
"hashes": [
"sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42",
"sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2"
"sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708",
"sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376"
],
"index": "pypi",
"version": "==4.1.1"
"version": "==4.2.0"
},
"urllib3": {
"hashes": [
"sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14",
"sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'",
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_full_version < '4.0.0'",
"version": "==1.26.9"
},
"virtualenv": {
+3
View File
@@ -0,0 +1,3 @@
Updated vendor version of ``pip`` from ``21.2.2`` to ``22.0.4`` which fixes a number of bugs including
several reports of pipenv locking for an infinite amount of time when using certain package constraints.
This also drops support for python 3.6 as it is EOL and support was removed in pip 22.x
+1 -1
View File
@@ -1,6 +1,6 @@
from typing import List, Optional
__version__ = "21.2.4"
__version__ = "22.0.4"
def main(args: Optional[List[str]] = None) -> int:
+96 -94
View File
@@ -31,15 +31,13 @@ logger = logging.getLogger(__name__)
class _Prefix:
def __init__(self, path):
# type: (str) -> None
def __init__(self, path: str) -> None:
self.path = path
self.setup = False
self.bin_dir = get_paths(
'nt' if os.name == 'nt' else 'posix_prefix',
vars={'base': path, 'platbase': path}
)['scripts']
"nt" if os.name == "nt" else "posix_prefix",
vars={"base": path, "platbase": path},
)["scripts"]
self.lib_dirs = get_prefixed_libs(path)
@@ -70,22 +68,18 @@ def _create_standalone_pip() -> Iterator[str]:
class BuildEnvironment:
"""Creates and manages an isolated environment to install build deps
"""
"""Creates and manages an isolated environment to install build deps"""
def __init__(self):
# type: () -> None
temp_dir = TempDirectory(
kind=tempdir_kinds.BUILD_ENV, globally_managed=True
)
def __init__(self) -> None:
temp_dir = TempDirectory(kind=tempdir_kinds.BUILD_ENV, globally_managed=True)
self._prefixes = OrderedDict(
(name, _Prefix(os.path.join(temp_dir.path, name)))
for name in ('normal', 'overlay')
for name in ("normal", "overlay")
)
self._bin_dirs = [] # type: List[str]
self._lib_dirs = [] # type: List[str]
self._bin_dirs: List[str] = []
self._lib_dirs: List[str] = []
for prefix in reversed(list(self._prefixes.values())):
self._bin_dirs.append(prefix.bin_dir)
self._lib_dirs.extend(prefix.lib_dirs)
@@ -96,12 +90,15 @@ class BuildEnvironment:
system_sites = {
os.path.normcase(site) for site in (get_purelib(), get_platlib())
}
self._site_dir = os.path.join(temp_dir.path, 'site')
self._site_dir = os.path.join(temp_dir.path, "site")
if not os.path.exists(self._site_dir):
os.mkdir(self._site_dir)
with open(os.path.join(self._site_dir, 'sitecustomize.py'), 'w') as fp:
fp.write(textwrap.dedent(
'''
with open(
os.path.join(self._site_dir, "sitecustomize.py"), "w", encoding="utf-8"
) as fp:
fp.write(
textwrap.dedent(
"""
import os, site, sys
# First, drop system-sites related paths.
@@ -124,47 +121,49 @@ class BuildEnvironment:
for path in {lib_dirs!r}:
assert not path in sys.path
site.addsitedir(path)
'''
).format(system_sites=system_sites, lib_dirs=self._lib_dirs))
"""
).format(system_sites=system_sites, lib_dirs=self._lib_dirs)
)
def __enter__(self):
# type: () -> None
def __enter__(self) -> None:
self._save_env = {
name: os.environ.get(name, None)
for name in ('PATH', 'PYTHONNOUSERSITE', 'PYTHONPATH')
for name in ("PATH", "PYTHONNOUSERSITE", "PYTHONPATH")
}
path = self._bin_dirs[:]
old_path = self._save_env['PATH']
old_path = self._save_env["PATH"]
if old_path:
path.extend(old_path.split(os.pathsep))
pythonpath = [self._site_dir]
os.environ.update({
'PATH': os.pathsep.join(path),
'PYTHONNOUSERSITE': '1',
'PYTHONPATH': os.pathsep.join(pythonpath),
})
os.environ.update(
{
"PATH": os.pathsep.join(path),
"PYTHONNOUSERSITE": "1",
"PYTHONPATH": os.pathsep.join(pythonpath),
}
)
def __exit__(
self,
exc_type, # type: Optional[Type[BaseException]]
exc_val, # type: Optional[BaseException]
exc_tb # type: Optional[TracebackType]
):
# type: (...) -> None
exc_type: Optional[Type[BaseException]],
exc_val: Optional[BaseException],
exc_tb: Optional[TracebackType],
) -> None:
for varname, old_value in self._save_env.items():
if old_value is None:
os.environ.pop(varname, None)
else:
os.environ[varname] = old_value
def check_requirements(self, reqs):
# type: (Iterable[str]) -> Tuple[Set[Tuple[str, str]], Set[str]]
def check_requirements(
self, reqs: Iterable[str]
) -> Tuple[Set[Tuple[str, str]], Set[str]]:
"""Return 2 sets:
- conflicting requirements: set of (installed, wanted) reqs tuples
- missing requirements: set of reqs
- conflicting requirements: set of (installed, wanted) reqs tuples
- missing requirements: set of reqs
"""
missing = set()
conflicting = set()
@@ -187,32 +186,25 @@ class BuildEnvironment:
def install_requirements(
self,
finder, # type: PackageFinder
requirements, # type: Iterable[str]
prefix_as_string, # type: str
message # type: str
):
# type: (...) -> None
finder: "PackageFinder",
requirements: Iterable[str],
prefix_as_string: str,
*,
kind: str,
) -> None:
prefix = self._prefixes[prefix_as_string]
assert not prefix.setup
prefix.setup = True
if not requirements:
return
with contextlib.ExitStack() as ctx:
# TODO: Remove this block when dropping 3.6 support. Python 3.6
# lacks importlib.resources and pep517 has issues loading files in
# a zip, so we fallback to the "old" method by adding the current
# pip directory to the child process's sys.path.
if sys.version_info < (3, 7):
pip_runnable = os.path.dirname(pip_location)
else:
pip_runnable = ctx.enter_context(_create_standalone_pip())
pip_runnable = ctx.enter_context(_create_standalone_pip())
self._install_requirements(
pip_runnable,
finder,
requirements,
prefix,
message,
kind=kind,
)
@staticmethod
@@ -221,75 +213,85 @@ class BuildEnvironment:
finder: "PackageFinder",
requirements: Iterable[str],
prefix: _Prefix,
message: str,
*,
kind: str,
) -> None:
sys_executable = os.environ.get('PIP_PYTHON_PATH', sys.executable)
args = [
sys_executable, pip_runnable, 'install',
'--ignore-installed', '--no-user', '--prefix', prefix.path,
'--no-warn-script-location',
] # type: List[str]
args: List[str] = [
sys_executable,
pip_runnable,
"install",
"--ignore-installed",
"--no-user",
"--prefix",
prefix.path,
"--no-warn-script-location",
]
if logger.getEffectiveLevel() <= logging.DEBUG:
args.append('-v')
for format_control in ('no_binary', 'only_binary'):
args.append("-v")
for format_control in ("no_binary", "only_binary"):
formats = getattr(finder.format_control, format_control)
args.extend(('--' + format_control.replace('_', '-'),
','.join(sorted(formats or {':none:'}))))
args.extend(
(
"--" + format_control.replace("_", "-"),
",".join(sorted(formats or {":none:"})),
)
)
index_urls = finder.index_urls
if index_urls:
args.extend(['-i', index_urls[0]])
args.extend(["-i", index_urls[0]])
for extra_index in index_urls[1:]:
args.extend(['--extra-index-url', extra_index])
args.extend(["--extra-index-url", extra_index])
else:
args.append('--no-index')
args.append("--no-index")
for link in finder.find_links:
args.extend(['--find-links', link])
args.extend(["--find-links", link])
for host in finder.trusted_hosts:
args.extend(['--trusted-host', host])
args.extend(["--trusted-host", host])
if finder.allow_all_prereleases:
args.append('--pre')
args.append("--pre")
if finder.prefer_binary:
args.append('--prefer-binary')
args.append('--')
args.append("--prefer-binary")
args.append("--")
args.extend(requirements)
extra_environ = {"_PIP_STANDALONE_CERT": where()}
with open_spinner(message) as spinner:
call_subprocess(args, spinner=spinner, extra_environ=extra_environ)
with open_spinner(f"Installing {kind}") as spinner:
call_subprocess(
args,
command_desc=f"pip subprocess to install {kind}",
spinner=spinner,
extra_environ=extra_environ,
)
class NoOpBuildEnvironment(BuildEnvironment):
"""A no-op drop-in replacement for BuildEnvironment
"""
"""A no-op drop-in replacement for BuildEnvironment"""
def __init__(self):
# type: () -> None
def __init__(self) -> None:
pass
def __enter__(self):
# type: () -> None
def __enter__(self) -> None:
pass
def __exit__(
self,
exc_type, # type: Optional[Type[BaseException]]
exc_val, # type: Optional[BaseException]
exc_tb # type: Optional[TracebackType]
):
# type: (...) -> None
exc_type: Optional[Type[BaseException]],
exc_val: Optional[BaseException],
exc_tb: Optional[TracebackType],
) -> None:
pass
def cleanup(self):
# type: () -> None
def cleanup(self) -> None:
pass
def install_requirements(
self,
finder, # type: PackageFinder
requirements, # type: Iterable[str]
prefix_as_string, # type: str
message # type: str
):
# type: (...) -> None
finder: "PackageFinder",
requirements: Iterable[str],
prefix_as_string: str,
*,
kind: str,
) -> None:
raise NotImplementedError()
+47 -70
View File
@@ -20,8 +20,7 @@ from pipenv.patched.notpip._internal.utils.urls import path_to_url
logger = logging.getLogger(__name__)
def _hash_dict(d):
# type: (Dict[str, str]) -> str
def _hash_dict(d: Dict[str, str]) -> str:
"""Return a stable sha224 of a dictionary."""
s = json.dumps(d, sort_keys=True, separators=(",", ":"), ensure_ascii=True)
return hashlib.sha224(s.encode("ascii")).hexdigest()
@@ -31,15 +30,16 @@ class Cache:
"""An abstract class - provides cache directories for data from links
:param cache_dir: The root of the cache.
:param format_control: An object of FormatControl class to limit
binaries being read from the cache.
:param allowed_formats: which formats of files the cache should store.
('binary' and 'source' are the only allowed values)
:param cache_dir: The root of the cache.
:param format_control: An object of FormatControl class to limit
binaries being read from the cache.
:param allowed_formats: which formats of files the cache should store.
('binary' and 'source' are the only allowed values)
"""
def __init__(self, cache_dir, format_control, allowed_formats):
# type: (str, FormatControl, Set[str]) -> None
def __init__(
self, cache_dir: str, format_control: FormatControl, allowed_formats: Set[str]
) -> None:
super().__init__()
assert not cache_dir or os.path.isabs(cache_dir)
self.cache_dir = cache_dir or None
@@ -49,10 +49,8 @@ class Cache:
_valid_formats = {"source", "binary"}
assert self.allowed_formats.union(_valid_formats) == _valid_formats
def _get_cache_path_parts(self, link):
# type: (Link) -> List[str]
"""Get parts of part that must be os.path.joined with cache_dir
"""
def _get_cache_path_parts(self, link: Link) -> List[str]:
"""Get parts of part that must be os.path.joined with cache_dir"""
# We want to generate an url to use as our cache key, we don't want to
# just re-use the URL because it might have other items in the fragment
@@ -84,19 +82,12 @@ class Cache:
return parts
def _get_candidates(self, link, canonical_package_name):
# type: (Link, str) -> List[Any]
can_not_cache = (
not self.cache_dir or
not canonical_package_name or
not link
)
def _get_candidates(self, link: Link, canonical_package_name: str) -> List[Any]:
can_not_cache = not self.cache_dir or not canonical_package_name or not link
if can_not_cache:
return []
formats = self.format_control.get_allowed_formats(
canonical_package_name
)
formats = self.format_control.get_allowed_formats(canonical_package_name)
if not self.allowed_formats.intersection(formats):
return []
@@ -107,19 +98,16 @@ class Cache:
candidates.append((candidate, path))
return candidates
def get_path_for_link(self, link):
# type: (Link) -> str
"""Return a directory to store cached items in for link.
"""
def get_path_for_link(self, link: Link) -> str:
"""Return a directory to store cached items in for link."""
raise NotImplementedError()
def get(
self,
link, # type: Link
package_name, # type: Optional[str]
supported_tags, # type: List[Tag]
):
# type: (...) -> Link
link: Link,
package_name: Optional[str],
supported_tags: List[Tag],
) -> Link:
"""Returns a link to a cached item if it exists, otherwise returns the
passed link.
"""
@@ -127,15 +115,12 @@ class Cache:
class SimpleWheelCache(Cache):
"""A cache of wheels for future installs.
"""
"""A cache of wheels for future installs."""
def __init__(self, cache_dir, format_control):
# type: (str, FormatControl) -> None
def __init__(self, cache_dir: str, format_control: FormatControl) -> None:
super().__init__(cache_dir, format_control, {"binary"})
def get_path_for_link(self, link):
# type: (Link) -> str
def get_path_for_link(self, link: Link) -> str:
"""Return a directory to store cached wheels for link
Because there are M wheels for any one sdist, we provide a directory
@@ -157,20 +142,17 @@ class SimpleWheelCache(Cache):
def get(
self,
link, # type: Link
package_name, # type: Optional[str]
supported_tags, # type: List[Tag]
):
# type: (...) -> Link
link: Link,
package_name: Optional[str],
supported_tags: List[Tag],
) -> Link:
candidates = []
if not package_name:
return link
canonical_package_name = canonicalize_name(package_name)
for wheel_name, wheel_dir in self._get_candidates(
link, canonical_package_name
):
for wheel_name, wheel_dir in self._get_candidates(link, canonical_package_name):
try:
wheel = Wheel(wheel_name)
except InvalidWheelFilename:
@@ -179,7 +161,9 @@ class SimpleWheelCache(Cache):
logger.debug(
"Ignoring cached wheel %s for %s as it "
"does not match the expected distribution name %s.",
wheel_name, link, package_name,
wheel_name,
link,
package_name,
)
continue
if not wheel.supported(supported_tags):
@@ -201,11 +185,9 @@ class SimpleWheelCache(Cache):
class EphemWheelCache(SimpleWheelCache):
"""A SimpleWheelCache that creates it's own temporary cache directory
"""
"""A SimpleWheelCache that creates it's own temporary cache directory"""
def __init__(self, format_control):
# type: (FormatControl) -> None
def __init__(self, format_control: FormatControl) -> None:
self._temp_dir = TempDirectory(
kind=tempdir_kinds.EPHEM_WHEEL_CACHE,
globally_managed=True,
@@ -217,8 +199,8 @@ class EphemWheelCache(SimpleWheelCache):
class CacheEntry:
def __init__(
self,
link, # type: Link
persistent, # type: bool
link: Link,
persistent: bool,
):
self.link = link
self.persistent = persistent
@@ -231,27 +213,23 @@ class WheelCache(Cache):
when a certain link is not found in the simple wheel cache first.
"""
def __init__(self, cache_dir, format_control):
# type: (str, FormatControl) -> None
super().__init__(cache_dir, format_control, {'binary'})
def __init__(self, cache_dir: str, format_control: FormatControl) -> None:
super().__init__(cache_dir, format_control, {"binary"})
self._wheel_cache = SimpleWheelCache(cache_dir, format_control)
self._ephem_cache = EphemWheelCache(format_control)
def get_path_for_link(self, link):
# type: (Link) -> str
def get_path_for_link(self, link: Link) -> str:
return self._wheel_cache.get_path_for_link(link)
def get_ephem_path_for_link(self, link):
# type: (Link) -> str
def get_ephem_path_for_link(self, link: Link) -> str:
return self._ephem_cache.get_path_for_link(link)
def get(
self,
link, # type: Link
package_name, # type: Optional[str]
supported_tags, # type: List[Tag]
):
# type: (...) -> Link
link: Link,
package_name: Optional[str],
supported_tags: List[Tag],
) -> Link:
cache_entry = self.get_cache_entry(link, package_name, supported_tags)
if cache_entry is None:
return link
@@ -259,11 +237,10 @@ class WheelCache(Cache):
def get_cache_entry(
self,
link, # type: Link
package_name, # type: Optional[str]
supported_tags, # type: List[Tag]
):
# type: (...) -> Optional[CacheEntry]
link: Link,
package_name: Optional[str],
supported_tags: List[Tag],
) -> Optional[CacheEntry]:
"""Returns a CacheEntry with a link to a cached item if it exists or
None. The cache entry indicates if the item was found in the persistent
or ephemeral cache.
@@ -59,6 +59,14 @@ def autocomplete() -> None:
print(dist)
sys.exit(1)
should_list_installables = (
not current.startswith("-") and subcommand_name == "install"
)
if should_list_installables:
for path in auto_complete_paths(current, "path"):
print(path)
sys.exit(1)
subcommand = create_command(subcommand_name)
for opt in subcommand.parser.option_list_all:
@@ -138,7 +146,7 @@ def auto_complete_paths(current: str, completion_type: str) -> Iterable[str]:
starting with ``current``.
:param current: The word to be completed
:param completion_type: path completion type(`file`, `path` or `dir`)i
:param completion_type: path completion type(``file``, ``path`` or ``dir``)
:return: A generator of regular files and/or directories
"""
directory, filename = os.path.split(current)
@@ -1,5 +1,6 @@
"""Base Command class, and related routines"""
import functools
import logging
import logging.config
import optparse
@@ -7,7 +8,9 @@ import os
import sys
import traceback
from optparse import Values
from typing import Any, List, Optional, Tuple
from typing import Any, Callable, List, Optional, Tuple
from pipenv.patched.notpip._vendor.rich import traceback as rich_traceback
from pipenv.patched.notpip._internal.cli import cmdoptions
from pipenv.patched.notpip._internal.cli.command_context import CommandContextMixIn
@@ -21,12 +24,12 @@ from pipenv.patched.notpip._internal.cli.status_codes import (
from pipenv.patched.notpip._internal.exceptions import (
BadCommand,
CommandError,
DiagnosticPipError,
InstallationError,
NetworkConnectionError,
PreviousBuildDirError,
UninstallationError,
)
from pipenv.patched.notpip._internal.utils.deprecation import deprecated
from pipenv.patched.notpip._internal.utils.filesystem import check_path_owner
from pipenv.patched.notpip._internal.utils.logging import BrokenStdoutLoggingError, setup_logging
from pipenv.patched.notpip._internal.utils.misc import get_prog, normalize_path
@@ -85,10 +88,10 @@ class Command(CommandContextMixIn):
# are present.
assert not hasattr(options, "no_index")
def run(self, options: Values, args: List[Any]) -> int:
def run(self, options: Values, args: List[str]) -> int:
raise NotImplementedError
def parse_args(self, args: List[str]) -> Tuple[Any, Any]:
def parse_args(self, args: List[str]) -> Tuple[Values, List[str]]:
# factored out for testability
return self.parser.parse_args(args)
@@ -148,20 +151,6 @@ class Command(CommandContextMixIn):
)
options.cache_dir = None
if getattr(options, "build_dir", None):
deprecated(
reason=(
"The -b/--build/--build-dir/--build-directory "
"option is deprecated and has no effect anymore."
),
replacement=(
"use the TMPDIR/TEMP/TMP environment variable, "
"possibly combined with --no-clean"
),
gone_in="21.3",
issue=8333,
)
if "2020-resolver" in options.features_enabled:
logger.warning(
"--use-feature=2020-resolver no longer has any effect, "
@@ -169,46 +158,66 @@ class Command(CommandContextMixIn):
"This will become an error in pip 21.0."
)
def intercepts_unhandled_exc(
run_func: Callable[..., int]
) -> Callable[..., int]:
@functools.wraps(run_func)
def exc_logging_wrapper(*args: Any) -> int:
try:
status = run_func(*args)
assert isinstance(status, int)
return status
except DiagnosticPipError as exc:
logger.error("[present-diagnostic] %s", exc)
logger.debug("Exception information:", exc_info=True)
return ERROR
except PreviousBuildDirError as exc:
logger.critical(str(exc))
logger.debug("Exception information:", exc_info=True)
return PREVIOUS_BUILD_DIR_ERROR
except (
InstallationError,
UninstallationError,
BadCommand,
NetworkConnectionError,
) as exc:
logger.critical(str(exc))
logger.debug("Exception information:", exc_info=True)
return ERROR
except CommandError as exc:
logger.critical("%s", exc)
logger.debug("Exception information:", exc_info=True)
return ERROR
except BrokenStdoutLoggingError:
# Bypass our logger and write any remaining messages to
# stderr because stdout no longer works.
print("ERROR: Pipe to stdout was broken", file=sys.stderr)
if level_number <= logging.DEBUG:
traceback.print_exc(file=sys.stderr)
return ERROR
except KeyboardInterrupt:
logger.critical("Operation cancelled by user")
logger.debug("Exception information:", exc_info=True)
return ERROR
except BaseException:
logger.critical("Exception:", exc_info=True)
return UNKNOWN_ERROR
return exc_logging_wrapper
try:
status = self.run(options, args)
assert isinstance(status, int)
return status
except PreviousBuildDirError as exc:
logger.critical(str(exc))
logger.debug("Exception information:", exc_info=True)
return PREVIOUS_BUILD_DIR_ERROR
except (
InstallationError,
UninstallationError,
BadCommand,
NetworkConnectionError,
) as exc:
logger.critical(str(exc))
logger.debug("Exception information:", exc_info=True)
return ERROR
except CommandError as exc:
logger.critical("%s", exc)
logger.debug("Exception information:", exc_info=True)
return ERROR
except BrokenStdoutLoggingError:
# Bypass our logger and write any remaining messages to stderr
# because stdout no longer works.
print("ERROR: Pipe to stdout was broken", file=sys.stderr)
if level_number <= logging.DEBUG:
traceback.print_exc(file=sys.stderr)
return ERROR
except KeyboardInterrupt:
logger.critical("Operation cancelled by user")
logger.debug("Exception information:", exc_info=True)
return ERROR
except BaseException:
logger.critical("Exception:", exc_info=True)
return UNKNOWN_ERROR
if not options.debug_mode:
run = intercepts_unhandled_exc(self.run)
else:
run = self.run
rich_traceback.install(show_locals=True)
return run(options, args)
finally:
self.handle_pip_version_check(options)
@@ -10,9 +10,9 @@ pass on state. To be consistent, all options will follow this design.
# The following comment should be removed at some point in the future.
# mypy: strict-optional=False
import logging
import os
import textwrap
import warnings
from functools import partial
from optparse import SUPPRESS_HELP, Option, OptionGroup, OptionParser, Values
from textwrap import dedent
@@ -30,6 +30,8 @@ from pipenv.patched.notpip._internal.models.target_python import TargetPython
from pipenv.patched.notpip._internal.utils.hashes import STRONG_HASHES
from pipenv.patched.notpip._internal.utils.misc import strtobool
logger = logging.getLogger(__name__)
def raise_option_error(parser: OptionParser, option: Option, msg: str) -> None:
"""
@@ -76,10 +78,9 @@ def check_install_build_global(
if any(map(getname, names)):
control = options.format_control
control.disallow_binaries()
warnings.warn(
logger.warning(
"Disabling all use of wheels due to the use of --build-option "
"/ --global-option / --install-option.",
stacklevel=2,
)
@@ -151,6 +152,18 @@ help_: Callable[..., Option] = partial(
help="Show help.",
)
debug_mode: Callable[..., Option] = partial(
Option,
"--debug",
dest="debug_mode",
action="store_true",
default=False,
help=(
"Let unhandled exceptions propagate outside the main subroutine, "
"instead of logging them to stderr."
),
)
isolated_mode: Callable[..., Option] = partial(
Option,
"--isolated",
@@ -165,13 +178,15 @@ isolated_mode: Callable[..., Option] = partial(
require_virtualenv: Callable[..., Option] = partial(
Option,
# Run only if inside a virtualenv, bail if not.
"--require-virtualenv",
"--require-venv",
dest="require_venv",
action="store_true",
default=False,
help=SUPPRESS_HELP,
help=(
"Allow pip to only run in a virtual environment; "
"exit with an error otherwise."
),
)
verbose: Callable[..., Option] = partial(
@@ -719,18 +734,6 @@ no_deps: Callable[..., Option] = partial(
help="Don't install package dependencies.",
)
build_dir: Callable[..., Option] = partial(
PipOption,
"-b",
"--build",
"--build-dir",
"--build-directory",
dest="build_dir",
type="path",
metavar="dir",
help=SUPPRESS_HELP,
)
ignore_requires_python: Callable[..., Option] = partial(
Option,
"--ignore-requires-python",
@@ -961,7 +964,12 @@ use_deprecated_feature: Callable[..., Option] = partial(
metavar="feature",
action="append",
default=[],
choices=["legacy-resolver"],
choices=[
"legacy-resolver",
"out-of-tree-build",
"backtrack-on-build-failures",
"html5lib",
],
help=("Enable deprecated functionality, that will be removed in the future."),
)
@@ -974,6 +982,7 @@ general_group: Dict[str, Any] = {
"name": "General Options",
"options": [
help_,
debug_mode,
isolated_mode,
require_virtualenv,
verbose,
@@ -1,10 +1,23 @@
import functools
import itertools
import sys
from signal import SIGINT, default_int_handler, signal
from typing import Any
from typing import Any, Callable, Iterator, Optional, Tuple
from pipenv.patched.notpip._vendor.progress.bar import Bar, FillingCirclesBar, IncrementalBar
from pipenv.patched.notpip._vendor.progress.spinner import Spinner
from pipenv.patched.notpip._vendor.rich.progress import (
BarColumn,
DownloadColumn,
FileSizeColumn,
Progress,
ProgressColumn,
SpinnerColumn,
TextColumn,
TimeElapsedColumn,
TimeRemainingColumn,
TransferSpeedColumn,
)
from pipenv.patched.notpip._internal.utils.compat import WINDOWS
from pipenv.patched.notpip._internal.utils.logging import get_indentation
@@ -17,6 +30,8 @@ try:
except Exception:
colorama = None
DownloadProgressRenderer = Callable[[Iterator[bytes]], Iterator[bytes]]
def _select_progress_class(preferred: Bar, fallback: Bar) -> Bar:
encoding = getattr(preferred.file, "encoding", None)
@@ -243,8 +258,64 @@ BAR_TYPES = {
}
def DownloadProgressProvider(progress_bar, max=None): # type: ignore
def _legacy_progress_bar(
progress_bar: str, max: Optional[int]
) -> DownloadProgressRenderer:
if max is None or max == 0:
return BAR_TYPES[progress_bar][1]().iter
return BAR_TYPES[progress_bar][1]().iter # type: ignore
else:
return BAR_TYPES[progress_bar][0](max=max).iter
#
# Modern replacement, for our legacy progress bars.
#
def _rich_progress_bar(
iterable: Iterator[bytes],
*,
bar_type: str,
size: int,
) -> Iterator[bytes]:
assert bar_type == "on", "This should only be used in the default mode."
if not size:
total = float("inf")
columns: Tuple[ProgressColumn, ...] = (
TextColumn("[progress.description]{task.description}"),
SpinnerColumn("line", speed=1.5),
FileSizeColumn(),
TransferSpeedColumn(),
TimeElapsedColumn(),
)
else:
total = size
columns = (
TextColumn("[progress.description]{task.description}"),
BarColumn(),
DownloadColumn(),
TransferSpeedColumn(),
TextColumn("eta"),
TimeRemainingColumn(),
)
progress = Progress(*columns, refresh_per_second=30)
task_id = progress.add_task(" " * (get_indentation() + 2), total=total)
with progress:
for chunk in iterable:
yield chunk
progress.update(task_id, advance=len(chunk))
def get_download_progress_renderer(
*, bar_type: str, size: Optional[int] = None
) -> DownloadProgressRenderer:
"""Get an object that can be used to render the download progress.
Returns a callable, that takes an iterable to "wrap".
"""
if bar_type == "on":
return functools.partial(_rich_progress_bar, bar_type=bar_type, size=size)
elif bar_type == "off":
return iter # no-op, when passed an iterator
else:
return _legacy_progress_bar(bar_type, size)
@@ -34,6 +34,7 @@ from pipenv.patched.notpip._internal.req.req_install import InstallRequirement
from pipenv.patched.notpip._internal.req.req_tracker import RequirementTracker
from pipenv.patched.notpip._internal.resolution.base import BaseResolver
from pipenv.patched.notpip._internal.self_outdated_check import pip_self_version_check
from pipenv.patched.notpip._internal.utils.deprecation import deprecated
from pipenv.patched.notpip._internal.utils.temp_dir import (
TempDirectory,
TempDirectoryTypeRegistry,
@@ -169,9 +170,10 @@ def warn_if_run_as_root() -> None:
# checks: https://mypy.readthedocs.io/en/stable/common_issues.html
if sys.platform == "win32" or sys.platform == "cygwin":
return
if sys.platform == "darwin" or sys.platform == "linux":
if os.getuid() != 0:
return
if os.getuid() != 0:
return
logger.warning(
"Running pip as the 'root' user can result in broken permissions and "
"conflicting behaviour with the system package manager. "
@@ -222,6 +224,31 @@ class RequirementCommand(IndexGroupCommand):
return "2020-resolver"
@staticmethod
def determine_build_failure_suppression(options: Values) -> bool:
"""Determines whether build failures should be suppressed and backtracked on."""
if "backtrack-on-build-failures" not in options.deprecated_features_enabled:
return False
if "legacy-resolver" in options.deprecated_features_enabled:
raise CommandError("Cannot backtrack with legacy resolver.")
deprecated(
reason=(
"Backtracking on build failures can mask issues related to how "
"a package generates metadata or builds a wheel. This flag will "
"be removed in pip 22.2."
),
gone_in=None,
replacement=(
"avoiding known-bad versions by explicitly telling pip to ignore them "
"(either directly as requirements, or via a constraints file)"
),
feature_flag=None,
issue=10655,
)
return True
@classmethod
def make_requirement_preparer(
cls,
@@ -232,6 +259,7 @@ class RequirementCommand(IndexGroupCommand):
finder: PackageFinder,
use_user_site: bool,
download_dir: Optional[str] = None,
verbosity: int = 0,
) -> RequirementPreparer:
"""
Create a RequirementPreparer instance for the given parameters.
@@ -257,6 +285,27 @@ class RequirementCommand(IndexGroupCommand):
"fast-deps has no effect when used with the legacy resolver."
)
in_tree_build = "out-of-tree-build" not in options.deprecated_features_enabled
if "in-tree-build" in options.features_enabled:
deprecated(
reason="In-tree builds are now the default.",
replacement="to remove the --use-feature=in-tree-build flag",
gone_in="22.1",
)
if "out-of-tree-build" in options.deprecated_features_enabled:
deprecated(
reason="Out-of-tree builds are deprecated.",
replacement=None,
gone_in="22.1",
)
if options.progress_bar not in {"on", "off"}:
deprecated(
reason="Custom progress bar styles are deprecated",
replacement="to use the default progress bar style.",
gone_in="22.1",
)
return RequirementPreparer(
build_dir=temp_build_dir_path,
src_dir=options.src_dir,
@@ -269,7 +318,8 @@ class RequirementCommand(IndexGroupCommand):
require_hashes=options.require_hashes,
use_user_site=use_user_site,
lazy_wheel=lazy_wheel,
in_tree_build="in-tree-build" in options.features_enabled,
verbosity=verbosity,
in_tree_build=in_tree_build,
)
@classmethod
@@ -295,6 +345,7 @@ class RequirementCommand(IndexGroupCommand):
isolated=options.isolated_mode,
use_pep517=use_pep517,
)
suppress_build_failures = cls.determine_build_failure_suppression(options)
resolver_variant = cls.determine_resolver_variant(options)
# The long import name and duplicated invocation is needed to convince
# Mypy into correctly typechecking. Otherwise it would complain the
@@ -314,6 +365,7 @@ class RequirementCommand(IndexGroupCommand):
force_reinstall=force_reinstall,
upgrade_strategy=upgrade_strategy,
py_version_info=py_version_info,
suppress_build_failures=suppress_build_failures,
)
import pipenv.patched.notpip._internal.resolution.legacy.resolver
@@ -447,4 +499,5 @@ class RequirementCommand(IndexGroupCommand):
link_collector=link_collector,
selection_prefs=selection_prefs,
target_python=target_python,
use_deprecated_html5lib="html5lib" in options.deprecated_features_enabled,
)
@@ -3,87 +3,102 @@ Package containing all pip commands
"""
import importlib
from collections import OrderedDict, namedtuple
from collections import namedtuple
from typing import Any, Dict, Optional
from pipenv.patched.notpip._internal.cli.base_command import Command
CommandInfo = namedtuple('CommandInfo', 'module_path, class_name, summary')
CommandInfo = namedtuple("CommandInfo", "module_path, class_name, summary")
# The ordering matters for help display.
# Also, even though the module path starts with the same
# "pipenv.patched.notpip._internal.commands" prefix in each case, we include the full path
# because it makes testing easier (specifically when modifying commands_dict
# in test setup / teardown by adding info for a FakeCommand class defined
# in a test-related module).
# Finally, we need to pass an iterable of pairs here rather than a dict
# so that the ordering won't be lost when using Python 2.7.
commands_dict: Dict[str, CommandInfo] = OrderedDict([
('install', CommandInfo(
'pipenv.patched.notpip._internal.commands.install', 'InstallCommand',
'Install packages.',
)),
('download', CommandInfo(
'pipenv.patched.notpip._internal.commands.download', 'DownloadCommand',
'Download packages.',
)),
('uninstall', CommandInfo(
'pipenv.patched.notpip._internal.commands.uninstall', 'UninstallCommand',
'Uninstall packages.',
)),
('freeze', CommandInfo(
'pipenv.patched.notpip._internal.commands.freeze', 'FreezeCommand',
'Output installed packages in requirements format.',
)),
('list', CommandInfo(
'pipenv.patched.notpip._internal.commands.list', 'ListCommand',
'List installed packages.',
)),
('show', CommandInfo(
'pipenv.patched.notpip._internal.commands.show', 'ShowCommand',
'Show information about installed packages.',
)),
('check', CommandInfo(
'pipenv.patched.notpip._internal.commands.check', 'CheckCommand',
'Verify installed packages have compatible dependencies.',
)),
('config', CommandInfo(
'pipenv.patched.notpip._internal.commands.configuration', 'ConfigurationCommand',
'Manage local and global configuration.',
)),
('search', CommandInfo(
'pipenv.patched.notpip._internal.commands.search', 'SearchCommand',
'Search PyPI for packages.',
)),
('cache', CommandInfo(
'pipenv.patched.notpip._internal.commands.cache', 'CacheCommand',
# This dictionary does a bunch of heavy lifting for help output:
# - Enables avoiding additional (costly) imports for presenting `--help`.
# - The ordering matters for help display.
#
# Even though the module path starts with the same "pipenv.patched.notpip._internal.commands"
# prefix, the full path makes testing easier (specifically when modifying
# `commands_dict` in test setup / teardown).
commands_dict: Dict[str, CommandInfo] = {
"install": CommandInfo(
"pipenv.patched.notpip._internal.commands.install",
"InstallCommand",
"Install packages.",
),
"download": CommandInfo(
"pipenv.patched.notpip._internal.commands.download",
"DownloadCommand",
"Download packages.",
),
"uninstall": CommandInfo(
"pipenv.patched.notpip._internal.commands.uninstall",
"UninstallCommand",
"Uninstall packages.",
),
"freeze": CommandInfo(
"pipenv.patched.notpip._internal.commands.freeze",
"FreezeCommand",
"Output installed packages in requirements format.",
),
"list": CommandInfo(
"pipenv.patched.notpip._internal.commands.list",
"ListCommand",
"List installed packages.",
),
"show": CommandInfo(
"pipenv.patched.notpip._internal.commands.show",
"ShowCommand",
"Show information about installed packages.",
),
"check": CommandInfo(
"pipenv.patched.notpip._internal.commands.check",
"CheckCommand",
"Verify installed packages have compatible dependencies.",
),
"config": CommandInfo(
"pipenv.patched.notpip._internal.commands.configuration",
"ConfigurationCommand",
"Manage local and global configuration.",
),
"search": CommandInfo(
"pipenv.patched.notpip._internal.commands.search",
"SearchCommand",
"Search PyPI for packages.",
),
"cache": CommandInfo(
"pipenv.patched.notpip._internal.commands.cache",
"CacheCommand",
"Inspect and manage pip's wheel cache.",
)),
('index', CommandInfo(
'pipenv.patched.notpip._internal.commands.index', 'IndexCommand',
),
"index": CommandInfo(
"pipenv.patched.notpip._internal.commands.index",
"IndexCommand",
"Inspect information available from package indexes.",
)),
('wheel', CommandInfo(
'pipenv.patched.notpip._internal.commands.wheel', 'WheelCommand',
'Build wheels from your requirements.',
)),
('hash', CommandInfo(
'pipenv.patched.notpip._internal.commands.hash', 'HashCommand',
'Compute hashes of package archives.',
)),
('completion', CommandInfo(
'pipenv.patched.notpip._internal.commands.completion', 'CompletionCommand',
'A helper command used for command completion.',
)),
('debug', CommandInfo(
'pipenv.patched.notpip._internal.commands.debug', 'DebugCommand',
'Show information useful for debugging.',
)),
('help', CommandInfo(
'pipenv.patched.notpip._internal.commands.help', 'HelpCommand',
'Show help for commands.',
)),
])
),
"wheel": CommandInfo(
"pipenv.patched.notpip._internal.commands.wheel",
"WheelCommand",
"Build wheels from your requirements.",
),
"hash": CommandInfo(
"pipenv.patched.notpip._internal.commands.hash",
"HashCommand",
"Compute hashes of package archives.",
),
"completion": CommandInfo(
"pipenv.patched.notpip._internal.commands.completion",
"CompletionCommand",
"A helper command used for command completion.",
),
"debug": CommandInfo(
"pipenv.patched.notpip._internal.commands.debug",
"DebugCommand",
"Show information useful for debugging.",
),
"help": CommandInfo(
"pipenv.patched.notpip._internal.commands.help",
"HelpCommand",
"Show help for commands.",
),
}
def create_command(name: str, **kwargs: Any) -> Command:
@@ -39,17 +39,17 @@ class CacheCommand(Command):
def add_options(self) -> None:
self.cmd_opts.add_option(
'--format',
action='store',
dest='list_format',
"--format",
action="store",
dest="list_format",
default="human",
choices=('human', 'abspath'),
help="Select the output format among: human (default) or abspath"
choices=("human", "abspath"),
help="Select the output format among: human (default) or abspath",
)
self.parser.insert_option_group(0, self.cmd_opts)
def run(self, options: Values, args: List[Any]) -> int:
def run(self, options: Values, args: List[str]) -> int:
handlers = {
"dir": self.get_cache_dir,
"info": self.get_cache_info,
@@ -59,8 +59,7 @@ class CacheCommand(Command):
}
if not options.cache_dir:
logger.error("pip cache commands can not "
"function since cache is disabled.")
logger.error("pip cache commands can not function since cache is disabled.")
return ERROR
# Determine action
@@ -84,69 +83,73 @@ class CacheCommand(Command):
def get_cache_dir(self, options: Values, args: List[Any]) -> None:
if args:
raise CommandError('Too many arguments')
raise CommandError("Too many arguments")
logger.info(options.cache_dir)
def get_cache_info(self, options: Values, args: List[Any]) -> None:
if args:
raise CommandError('Too many arguments')
raise CommandError("Too many arguments")
num_http_files = len(self._find_http_files(options))
num_packages = len(self._find_wheels(options, '*'))
num_packages = len(self._find_wheels(options, "*"))
http_cache_location = self._cache_dir(options, 'http')
wheels_cache_location = self._cache_dir(options, 'wheels')
http_cache_location = self._cache_dir(options, "http")
wheels_cache_location = self._cache_dir(options, "wheels")
http_cache_size = filesystem.format_directory_size(http_cache_location)
wheels_cache_size = filesystem.format_directory_size(
wheels_cache_location
)
wheels_cache_size = filesystem.format_directory_size(wheels_cache_location)
message = textwrap.dedent("""
Package index page cache location: {http_cache_location}
Package index page cache size: {http_cache_size}
Number of HTTP files: {num_http_files}
Wheels location: {wheels_cache_location}
Wheels size: {wheels_cache_size}
Number of wheels: {package_count}
""").format(
http_cache_location=http_cache_location,
http_cache_size=http_cache_size,
num_http_files=num_http_files,
wheels_cache_location=wheels_cache_location,
package_count=num_packages,
wheels_cache_size=wheels_cache_size,
).strip()
message = (
textwrap.dedent(
"""
Package index page cache location: {http_cache_location}
Package index page cache size: {http_cache_size}
Number of HTTP files: {num_http_files}
Wheels location: {wheels_cache_location}
Wheels size: {wheels_cache_size}
Number of wheels: {package_count}
"""
)
.format(
http_cache_location=http_cache_location,
http_cache_size=http_cache_size,
num_http_files=num_http_files,
wheels_cache_location=wheels_cache_location,
package_count=num_packages,
wheels_cache_size=wheels_cache_size,
)
.strip()
)
logger.info(message)
def list_cache_items(self, options: Values, args: List[Any]) -> None:
if len(args) > 1:
raise CommandError('Too many arguments')
raise CommandError("Too many arguments")
if args:
pattern = args[0]
else:
pattern = '*'
pattern = "*"
files = self._find_wheels(options, pattern)
if options.list_format == 'human':
if options.list_format == "human":
self.format_for_human(files)
else:
self.format_for_abspath(files)
def format_for_human(self, files: List[str]) -> None:
if not files:
logger.info('Nothing cached.')
logger.info("Nothing cached.")
return
results = []
for filename in files:
wheel = os.path.basename(filename)
size = filesystem.format_file_size(filename)
results.append(f' - {wheel} ({size})')
logger.info('Cache contents:\n')
logger.info('\n'.join(sorted(results)))
results.append(f" - {wheel} ({size})")
logger.info("Cache contents:\n")
logger.info("\n".join(sorted(results)))
def format_for_abspath(self, files: List[str]) -> None:
if not files:
@@ -156,23 +159,27 @@ class CacheCommand(Command):
for filename in files:
results.append(filename)
logger.info('\n'.join(sorted(results)))
logger.info("\n".join(sorted(results)))
def remove_cache_items(self, options: Values, args: List[Any]) -> None:
if len(args) > 1:
raise CommandError('Too many arguments')
raise CommandError("Too many arguments")
if not args:
raise CommandError('Please provide a pattern')
raise CommandError("Please provide a pattern")
files = self._find_wheels(options, args[0])
# Only fetch http files if no specific pattern given
if args[0] == '*':
no_matching_msg = "No matching packages"
if args[0] == "*":
# Only fetch http files if no specific pattern given
files += self._find_http_files(options)
else:
# Add the pattern to the log message
no_matching_msg += ' for pattern "{}"'.format(args[0])
if not files:
raise CommandError('No matching packages')
logger.warning(no_matching_msg)
for filename in files:
os.unlink(filename)
@@ -181,19 +188,19 @@ class CacheCommand(Command):
def purge_cache(self, options: Values, args: List[Any]) -> None:
if args:
raise CommandError('Too many arguments')
raise CommandError("Too many arguments")
return self.remove_cache_items(options, ['*'])
return self.remove_cache_items(options, ["*"])
def _cache_dir(self, options: Values, subdir: str) -> str:
return os.path.join(options.cache_dir, subdir)
def _find_http_files(self, options: Values) -> List[str]:
http_dir = self._cache_dir(options, 'http')
return filesystem.find_files(http_dir, '*')
http_dir = self._cache_dir(options, "http")
return filesystem.find_files(http_dir, "*")
def _find_wheels(self, options: Values, pattern: str) -> List[str]:
wheel_dir = self._cache_dir(options, 'wheels')
wheel_dir = self._cache_dir(options, "wheels")
# The wheel filename format, as specified in PEP 427, is:
# {distribution}-{version}(-{build})?-{python}-{abi}-{platform}.whl
@@ -1,6 +1,6 @@
import logging
from optparse import Values
from typing import Any, List
from typing import List
from pipenv.patched.notpip._internal.cli.base_command import Command
from pipenv.patched.notpip._internal.cli.status_codes import ERROR, SUCCESS
@@ -19,7 +19,7 @@ class CheckCommand(Command):
usage = """
%prog [options]"""
def run(self, options: Values, args: List[Any]) -> int:
def run(self, options: Values, args: List[str]) -> int:
package_set, parsing_probs = create_package_set_from_installed()
missing, conflicting = check_package_set(package_set)
@@ -29,7 +29,9 @@ class CheckCommand(Command):
for dependency in missing[project_name]:
write_output(
"%s %s requires %s, which is not installed.",
project_name, version, dependency[0],
project_name,
version,
dependency[0],
)
for project_name in conflicting:
@@ -37,7 +39,11 @@ class CheckCommand(Command):
for dep_name, dep_version, req in conflicting[project_name]:
write_output(
"%s %s has requirement %s, but you have %s %s.",
project_name, version, req, dep_name, dep_version,
project_name,
version,
req,
dep_name,
dep_version,
)
if missing or conflicting or parsing_probs:
@@ -12,7 +12,7 @@ BASE_COMPLETION = """
"""
COMPLETION_SCRIPTS = {
'bash': """
"bash": """
_pip_completion()
{{
COMPREPLY=( $( COMP_WORDS="${{COMP_WORDS[*]}}" \\
@@ -21,7 +21,7 @@ COMPLETION_SCRIPTS = {
}}
complete -o default -F _pip_completion {prog}
""",
'zsh': """
"zsh": """
function _pip_completion {{
local words cword
read -Ac words
@@ -32,7 +32,7 @@ COMPLETION_SCRIPTS = {
}}
compctl -K _pip_completion {prog}
""",
'fish': """
"fish": """
function __fish_complete_pip
set -lx COMP_WORDS (commandline -o) ""
set -lx COMP_CWORD ( \\
@@ -53,39 +53,44 @@ class CompletionCommand(Command):
def add_options(self) -> None:
self.cmd_opts.add_option(
'--bash', '-b',
action='store_const',
const='bash',
dest='shell',
help='Emit completion code for bash')
"--bash",
"-b",
action="store_const",
const="bash",
dest="shell",
help="Emit completion code for bash",
)
self.cmd_opts.add_option(
'--zsh', '-z',
action='store_const',
const='zsh',
dest='shell',
help='Emit completion code for zsh')
"--zsh",
"-z",
action="store_const",
const="zsh",
dest="shell",
help="Emit completion code for zsh",
)
self.cmd_opts.add_option(
'--fish', '-f',
action='store_const',
const='fish',
dest='shell',
help='Emit completion code for fish')
"--fish",
"-f",
action="store_const",
const="fish",
dest="shell",
help="Emit completion code for fish",
)
self.parser.insert_option_group(0, self.cmd_opts)
def run(self, options: Values, args: List[str]) -> int:
"""Prints the completion code of the given shell"""
shells = COMPLETION_SCRIPTS.keys()
shell_options = ['--' + shell for shell in sorted(shells)]
shell_options = ["--" + shell for shell in sorted(shells)]
if options.shell in shells:
script = textwrap.dedent(
COMPLETION_SCRIPTS.get(options.shell, '').format(
prog=get_prog())
COMPLETION_SCRIPTS.get(options.shell, "").format(prog=get_prog())
)
print(BASE_COMPLETION.format(script=script, shell=options.shell))
return SUCCESS
else:
sys.stderr.write(
'ERROR: You must pass {}\n' .format(' or '.join(shell_options))
"ERROR: You must pass {}\n".format(" or ".join(shell_options))
)
return SUCCESS
@@ -34,7 +34,7 @@ class ConfigurationCommand(Command):
If none of --user, --global and --site are passed, a virtual
environment configuration file is used if one is active and the file
exists. Otherwise, all modifications happen on the to the user file by
exists. Otherwise, all modifications happen to the user file by
default.
"""
@@ -51,38 +51,38 @@ class ConfigurationCommand(Command):
def add_options(self) -> None:
self.cmd_opts.add_option(
'--editor',
dest='editor',
action='store',
"--editor",
dest="editor",
action="store",
default=None,
help=(
'Editor to use to edit the file. Uses VISUAL or EDITOR '
'environment variables if not provided.'
)
"Editor to use to edit the file. Uses VISUAL or EDITOR "
"environment variables if not provided."
),
)
self.cmd_opts.add_option(
'--global',
dest='global_file',
action='store_true',
"--global",
dest="global_file",
action="store_true",
default=False,
help='Use the system-wide configuration file only'
help="Use the system-wide configuration file only",
)
self.cmd_opts.add_option(
'--user',
dest='user_file',
action='store_true',
"--user",
dest="user_file",
action="store_true",
default=False,
help='Use the user configuration file only'
help="Use the user configuration file only",
)
self.cmd_opts.add_option(
'--site',
dest='site_file',
action='store_true',
"--site",
dest="site_file",
action="store_true",
default=False,
help='Use the current environment configuration file only'
help="Use the current environment configuration file only",
)
self.parser.insert_option_group(0, self.cmd_opts)
@@ -133,11 +133,15 @@ class ConfigurationCommand(Command):
return SUCCESS
def _determine_file(self, options: Values, need_value: bool) -> Optional[Kind]:
file_options = [key for key, value in (
(kinds.USER, options.user_file),
(kinds.GLOBAL, options.global_file),
(kinds.SITE, options.site_file),
) if value]
file_options = [
key
for key, value in (
(kinds.USER, options.user_file),
(kinds.GLOBAL, options.global_file),
(kinds.SITE, options.site_file),
)
if value
]
if not file_options:
if not need_value:
@@ -194,24 +198,22 @@ class ConfigurationCommand(Command):
for fname in files:
with indent_log():
file_exists = os.path.exists(fname)
write_output("%s, exists: %r",
fname, file_exists)
write_output("%s, exists: %r", fname, file_exists)
if file_exists:
self.print_config_file_values(variant)
def print_config_file_values(self, variant: Kind) -> None:
"""Get key-value pairs from the file of a variant"""
for name, value in self.configuration.\
get_values_in_config(variant).items():
for name, value in self.configuration.get_values_in_config(variant).items():
with indent_log():
write_output("%s: %s", name, value)
def print_env_var_values(self) -> None:
"""Get key-values pairs present as environment variables"""
write_output("%s:", 'env_var')
write_output("%s:", "env_var")
with indent_log():
for key, value in sorted(self.configuration.get_environ_vars()):
env_var = f'PIP_{key.upper()}'
env_var = f"PIP_{key.upper()}"
write_output("%s=%r", env_var, value)
def open_in_editor(self, options: Values, args: List[str]) -> None:
@@ -225,16 +227,14 @@ class ConfigurationCommand(Command):
subprocess.check_call([editor, fname])
except subprocess.CalledProcessError as e:
raise PipError(
"Editor Subprocess exited with exit code {}"
.format(e.returncode)
"Editor Subprocess exited with exit code {}".format(e.returncode)
)
def _get_n_args(self, args: List[str], example: str, n: int) -> Any:
"""Helper to make sure the command got the right number of arguments
"""
"""Helper to make sure the command got the right number of arguments"""
if len(args) != n:
msg = (
'Got unexpected number of arguments, expected {}. '
"Got unexpected number of arguments, expected {}. "
'(example: "{} config {}")'
).format(n, get_prog(), example)
raise PipError(msg)
@@ -24,52 +24,46 @@ logger = logging.getLogger(__name__)
def show_value(name: str, value: Any) -> None:
logger.info('%s: %s', name, value)
logger.info("%s: %s", name, value)
def show_sys_implementation() -> None:
logger.info('sys.implementation:')
logger.info("sys.implementation:")
implementation_name = sys.implementation.name
with indent_log():
show_value('name', implementation_name)
show_value("name", implementation_name)
def create_vendor_txt_map() -> Dict[str, str]:
vendor_txt_path = os.path.join(
os.path.dirname(pip_location),
'_vendor',
'vendor.txt'
os.path.dirname(pip_location), "_vendor", "vendor.txt"
)
with open(vendor_txt_path) as f:
# Purge non version specifying lines.
# Also, remove any space prefix or suffixes (including comments).
lines = [line.strip().split(' ', 1)[0]
for line in f.readlines() if '==' in line]
lines = [
line.strip().split(" ", 1)[0] for line in f.readlines() if "==" in line
]
# Transform into "module" -> version dict.
return dict(line.split('==', 1) for line in lines) # type: ignore
return dict(line.split("==", 1) for line in lines) # type: ignore
def get_module_from_module_name(module_name: str) -> ModuleType:
# Module name can be uppercase in vendor.txt for some reason...
module_name = module_name.lower()
# PATCH: setuptools is actually only pkg_resources.
if module_name == 'setuptools':
module_name = 'pkg_resources'
if module_name == "setuptools":
module_name = "pkg_resources"
__import__(
f'pipenv.patched.notpip._vendor.{module_name}',
globals(),
locals(),
level=0
)
__import__(f"pipenv.patched.notpip._vendor.{module_name}", globals(), locals(), level=0)
return getattr(pipenv.patched.notpip._vendor, module_name)
def get_vendor_version_from_module(module_name: str) -> Optional[str]:
module = get_module_from_module_name(module_name)
version = getattr(module, '__version__', None)
version = getattr(module, "__version__", None)
if not version:
# Try to find version in debundled module info.
@@ -86,20 +80,24 @@ def show_actual_vendor_versions(vendor_txt_versions: Dict[str, str]) -> None:
a conflict or if the actual version could not be imported.
"""
for module_name, expected_version in vendor_txt_versions.items():
extra_message = ''
extra_message = ""
actual_version = get_vendor_version_from_module(module_name)
if not actual_version:
extra_message = ' (Unable to locate actual module version, using'\
' vendor.txt specified version)'
extra_message = (
" (Unable to locate actual module version, using"
" vendor.txt specified version)"
)
actual_version = expected_version
elif parse_version(actual_version) != parse_version(expected_version):
extra_message = ' (CONFLICT: vendor.txt suggests version should'\
' be {})'.format(expected_version)
logger.info('%s==%s%s', module_name, actual_version, extra_message)
extra_message = (
" (CONFLICT: vendor.txt suggests version should"
" be {})".format(expected_version)
)
logger.info("%s==%s%s", module_name, actual_version, extra_message)
def show_vendor_versions() -> None:
logger.info('vendored library versions:')
logger.info("vendored library versions:")
vendor_txt_versions = create_vendor_txt_map()
with indent_log():
@@ -114,11 +112,11 @@ def show_tags(options: Values) -> None:
# Display the target options that were explicitly provided.
formatted_target = target_python.format_given()
suffix = ''
suffix = ""
if formatted_target:
suffix = f' (target: {formatted_target})'
suffix = f" (target: {formatted_target})"
msg = 'Compatible tags: {}{}'.format(len(tags), suffix)
msg = "Compatible tags: {}{}".format(len(tags), suffix)
logger.info(msg)
if options.verbose < 1 and len(tags) > tag_limit:
@@ -133,8 +131,7 @@ def show_tags(options: Values) -> None:
if tags_limited:
msg = (
'...\n'
'[First {tag_limit} tags shown. Pass --verbose to show all.]'
"...\n[First {tag_limit} tags shown. Pass --verbose to show all.]"
).format(tag_limit=tag_limit)
logger.info(msg)
@@ -142,20 +139,20 @@ def show_tags(options: Values) -> None:
def ca_bundle_info(config: Configuration) -> str:
levels = set()
for key, _ in config.items():
levels.add(key.split('.')[0])
levels.add(key.split(".")[0])
if not levels:
return "Not specified"
levels_that_override_global = ['install', 'wheel', 'download']
levels_that_override_global = ["install", "wheel", "download"]
global_overriding_level = [
level for level in levels if level in levels_that_override_global
]
if not global_overriding_level:
return 'global'
return "global"
if 'global' in levels:
levels.remove('global')
if "global" in levels:
levels.remove("global")
return ", ".join(levels)
@@ -180,20 +177,21 @@ class DebugCommand(Command):
"details, since the output and options of this command may "
"change without notice."
)
show_value('pip version', get_pip_version())
show_value('sys.version', sys.version)
show_value('sys.executable', sys.executable)
show_value('sys.getdefaultencoding', sys.getdefaultencoding())
show_value('sys.getfilesystemencoding', sys.getfilesystemencoding())
show_value("pip version", get_pip_version())
show_value("sys.version", sys.version)
show_value("sys.executable", sys.executable)
show_value("sys.getdefaultencoding", sys.getdefaultencoding())
show_value("sys.getfilesystemencoding", sys.getfilesystemencoding())
show_value(
'locale.getpreferredencoding', locale.getpreferredencoding(),
"locale.getpreferredencoding",
locale.getpreferredencoding(),
)
show_value('sys.platform', sys.platform)
show_value("sys.platform", sys.platform)
show_sys_implementation()
show_value("'cert' config value", ca_bundle_info(self.parser.config))
show_value("REQUESTS_CA_BUNDLE", os.environ.get('REQUESTS_CA_BUNDLE'))
show_value("CURL_CA_BUNDLE", os.environ.get('CURL_CA_BUNDLE'))
show_value("REQUESTS_CA_BUNDLE", os.environ.get("REQUESTS_CA_BUNDLE"))
show_value("CURL_CA_BUNDLE", os.environ.get("CURL_CA_BUNDLE"))
show_value("pipenv.patched.notpip._vendor.certifi.where()", where())
show_value("pipenv.patched.notpip._vendor.DEBUNDLED", pipenv.patched.notpip._vendor.DEBUNDLED)
@@ -37,7 +37,6 @@ class DownloadCommand(RequirementCommand):
def add_options(self) -> None:
self.cmd_opts.add_option(cmdoptions.constraints())
self.cmd_opts.add_option(cmdoptions.requirements())
self.cmd_opts.add_option(cmdoptions.build_dir())
self.cmd_opts.add_option(cmdoptions.no_deps())
self.cmd_opts.add_option(cmdoptions.global_options())
self.cmd_opts.add_option(cmdoptions.no_binary())
@@ -53,11 +52,14 @@ class DownloadCommand(RequirementCommand):
self.cmd_opts.add_option(cmdoptions.ignore_requires_python())
self.cmd_opts.add_option(
'-d', '--dest', '--destination-dir', '--destination-directory',
dest='download_dir',
metavar='dir',
"-d",
"--dest",
"--destination-dir",
"--destination-directory",
dest="download_dir",
metavar="dir",
default=os.curdir,
help=("Download packages into <dir>."),
help="Download packages into <dir>.",
)
cmdoptions.add_target_python_options(self.cmd_opts)
@@ -111,6 +113,7 @@ class DownloadCommand(RequirementCommand):
finder=finder,
download_dir=options.download_dir,
use_user_site=False,
verbosity=self.verbosity,
)
resolver = self.make_resolver(
@@ -123,9 +126,7 @@ class DownloadCommand(RequirementCommand):
self.trace_basic_info(finder)
requirement_set = resolver.resolve(
reqs, check_supported_wheels=True
)
requirement_set = resolver.resolve(reqs, check_supported_wheels=True)
downloaded: List[str] = []
for req in requirement_set.requirements.values():
@@ -134,6 +135,6 @@ class DownloadCommand(RequirementCommand):
preparer.save_linked_requirement(req)
downloaded.append(req.name)
if downloaded:
write_output('Successfully downloaded %s', ' '.join(downloaded))
write_output("Successfully downloaded %s", " ".join(downloaded))
return SUCCESS
@@ -8,7 +8,7 @@ from pipenv.patched.notpip._internal.cli.status_codes import SUCCESS
from pipenv.patched.notpip._internal.operations.freeze import freeze
from pipenv.patched.notpip._internal.utils.compat import stdlib_pkgs
DEV_PKGS = {'pip', 'setuptools', 'distribute', 'wheel'}
DEV_PKGS = {"pip", "setuptools", "distribute", "wheel"}
class FreezeCommand(Command):
@@ -24,39 +24,52 @@ class FreezeCommand(Command):
def add_options(self) -> None:
self.cmd_opts.add_option(
'-r', '--requirement',
dest='requirements',
action='append',
"-r",
"--requirement",
dest="requirements",
action="append",
default=[],
metavar='file',
help="Use the order in the given requirements file and its "
"comments when generating output. This option can be "
"used multiple times.")
metavar="file",
help=(
"Use the order in the given requirements file and its "
"comments when generating output. This option can be "
"used multiple times."
),
)
self.cmd_opts.add_option(
'-l', '--local',
dest='local',
action='store_true',
"-l",
"--local",
dest="local",
action="store_true",
default=False,
help='If in a virtualenv that has global access, do not output '
'globally-installed packages.')
help=(
"If in a virtualenv that has global access, do not output "
"globally-installed packages."
),
)
self.cmd_opts.add_option(
'--user',
dest='user',
action='store_true',
"--user",
dest="user",
action="store_true",
default=False,
help='Only output packages installed in user-site.')
help="Only output packages installed in user-site.",
)
self.cmd_opts.add_option(cmdoptions.list_path())
self.cmd_opts.add_option(
'--all',
dest='freeze_all',
action='store_true',
help='Do not skip these packages in the output:'
' {}'.format(', '.join(DEV_PKGS)))
"--all",
dest="freeze_all",
action="store_true",
help=(
"Do not skip these packages in the output:"
" {}".format(", ".join(DEV_PKGS))
),
)
self.cmd_opts.add_option(
'--exclude-editable',
dest='exclude_editable',
action='store_true',
help='Exclude editable package from output.')
"--exclude-editable",
dest="exclude_editable",
action="store_true",
help="Exclude editable package from output.",
)
self.cmd_opts.add_option(cmdoptions.list_exclude())
self.parser.insert_option_group(0, self.cmd_opts)
@@ -80,5 +93,5 @@ class FreezeCommand(Command):
skip=skip,
exclude_editable=options.exclude_editable,
):
sys.stdout.write(line + '\n')
sys.stdout.write(line + "\n")
return SUCCESS
@@ -20,18 +20,21 @@ class HashCommand(Command):
installs.
"""
usage = '%prog [options] <file> ...'
usage = "%prog [options] <file> ..."
ignore_require_venv = True
def add_options(self) -> None:
self.cmd_opts.add_option(
'-a', '--algorithm',
dest='algorithm',
"-a",
"--algorithm",
dest="algorithm",
choices=STRONG_HASHES,
action='store',
action="store",
default=FAVORITE_HASH,
help='The hash algorithm to use: one of {}'.format(
', '.join(STRONG_HASHES)))
help="The hash algorithm to use: one of {}".format(
", ".join(STRONG_HASHES)
),
)
self.parser.insert_option_group(0, self.cmd_opts)
def run(self, options: Values, args: List[str]) -> int:
@@ -41,14 +44,15 @@ class HashCommand(Command):
algorithm = options.algorithm
for path in args:
write_output('%s:\n--hash=%s:%s',
path, algorithm, _hash_of_file(path, algorithm))
write_output(
"%s:\n--hash=%s:%s", path, algorithm, _hash_of_file(path, algorithm)
)
return SUCCESS
def _hash_of_file(path: str, algorithm: str) -> str:
"""Return the hash digest of a file."""
with open(path, 'rb') as archive:
with open(path, "rb") as archive:
hash = hashlib.new(algorithm)
for chunk in read_chunks(archive):
hash.update(chunk)
@@ -33,7 +33,7 @@ class HelpCommand(Command):
if guess:
msg.append(f'maybe you meant "{guess}"')
raise CommandError(' - '.join(msg))
raise CommandError(" - ".join(msg))
command = create_command(cmd_name)
command.parser.print_help()
@@ -44,7 +44,7 @@ class IndexCommand(IndexGroupCommand):
self.parser.insert_option_group(0, index_opts)
self.parser.insert_option_group(0, self.cmd_opts)
def run(self, options: Values, args: List[Any]) -> int:
def run(self, options: Values, args: List[str]) -> int:
handlers = {
"versions": self.get_available_package_versions,
}
@@ -97,11 +97,12 @@ class IndexCommand(IndexGroupCommand):
link_collector=link_collector,
selection_prefs=selection_prefs,
target_python=target_python,
use_deprecated_html5lib="html5lib" in options.deprecated_features_enabled,
)
def get_available_package_versions(self, options: Values, args: List[Any]) -> None:
if len(args) != 1:
raise CommandError('You need to specify exactly one argument')
raise CommandError("You need to specify exactly one argument")
target_python = cmdoptions.make_target_python(options)
query = args[0]
@@ -115,25 +116,24 @@ class IndexCommand(IndexGroupCommand):
)
versions: Iterable[Union[LegacyVersion, Version]] = (
candidate.version
for candidate in finder.find_all_candidates(query)
candidate.version for candidate in finder.find_all_candidates(query)
)
if not options.pre:
# Remove prereleases
versions = (version for version in versions
if not version.is_prerelease)
versions = (
version for version in versions if not version.is_prerelease
)
versions = set(versions)
if not versions:
raise DistributionNotFound(
'No matching distribution found for {}'.format(query))
"No matching distribution found for {}".format(query)
)
formatted_versions = [str(ver) for ver in sorted(
versions, reverse=True)]
formatted_versions = [str(ver) for ver in sorted(versions, reverse=True)]
latest = formatted_versions[0]
write_output('{} ({})'.format(query, latest))
write_output('Available versions: {}'.format(
', '.join(formatted_versions)))
write_output("{} ({})".format(query, latest))
write_output("Available versions: {}".format(", ".join(formatted_versions)))
print_dist_installation_info(query, latest)
@@ -86,87 +86,103 @@ class InstallCommand(RequirementCommand):
self.cmd_opts.add_option(cmdoptions.editable())
self.cmd_opts.add_option(
'-t', '--target',
dest='target_dir',
metavar='dir',
"-t",
"--target",
dest="target_dir",
metavar="dir",
default=None,
help='Install packages into <dir>. '
'By default this will not replace existing files/folders in '
'<dir>. Use --upgrade to replace existing packages in <dir> '
'with new versions.'
help=(
"Install packages into <dir>. "
"By default this will not replace existing files/folders in "
"<dir>. Use --upgrade to replace existing packages in <dir> "
"with new versions."
),
)
cmdoptions.add_target_python_options(self.cmd_opts)
self.cmd_opts.add_option(
'--user',
dest='use_user_site',
action='store_true',
help="Install to the Python user install directory for your "
"platform. Typically ~/.local/, or %APPDATA%\\Python on "
"Windows. (See the Python documentation for site.USER_BASE "
"for full details.)")
"--user",
dest="use_user_site",
action="store_true",
help=(
"Install to the Python user install directory for your "
"platform. Typically ~/.local/, or %APPDATA%\\Python on "
"Windows. (See the Python documentation for site.USER_BASE "
"for full details.)"
),
)
self.cmd_opts.add_option(
'--no-user',
dest='use_user_site',
action='store_false',
help=SUPPRESS_HELP)
"--no-user",
dest="use_user_site",
action="store_false",
help=SUPPRESS_HELP,
)
self.cmd_opts.add_option(
'--root',
dest='root_path',
metavar='dir',
"--root",
dest="root_path",
metavar="dir",
default=None,
help="Install everything relative to this alternate root "
"directory.")
help="Install everything relative to this alternate root directory.",
)
self.cmd_opts.add_option(
'--prefix',
dest='prefix_path',
metavar='dir',
"--prefix",
dest="prefix_path",
metavar="dir",
default=None,
help="Installation prefix where lib, bin and other top-level "
"folders are placed")
self.cmd_opts.add_option(cmdoptions.build_dir())
help=(
"Installation prefix where lib, bin and other top-level "
"folders are placed"
),
)
self.cmd_opts.add_option(cmdoptions.src())
self.cmd_opts.add_option(
'-U', '--upgrade',
dest='upgrade',
action='store_true',
help='Upgrade all specified packages to the newest available '
'version. The handling of dependencies depends on the '
'upgrade-strategy used.'
"-U",
"--upgrade",
dest="upgrade",
action="store_true",
help=(
"Upgrade all specified packages to the newest available "
"version. The handling of dependencies depends on the "
"upgrade-strategy used."
),
)
self.cmd_opts.add_option(
'--upgrade-strategy',
dest='upgrade_strategy',
default='only-if-needed',
choices=['only-if-needed', 'eager'],
help='Determines how dependency upgrading should be handled '
'[default: %default]. '
'"eager" - dependencies are upgraded regardless of '
'whether the currently installed version satisfies the '
'requirements of the upgraded package(s). '
'"only-if-needed" - are upgraded only when they do not '
'satisfy the requirements of the upgraded package(s).'
"--upgrade-strategy",
dest="upgrade_strategy",
default="only-if-needed",
choices=["only-if-needed", "eager"],
help=(
"Determines how dependency upgrading should be handled "
"[default: %default]. "
'"eager" - dependencies are upgraded regardless of '
"whether the currently installed version satisfies the "
"requirements of the upgraded package(s). "
'"only-if-needed" - are upgraded only when they do not '
"satisfy the requirements of the upgraded package(s)."
),
)
self.cmd_opts.add_option(
'--force-reinstall',
dest='force_reinstall',
action='store_true',
help='Reinstall all packages even if they are already '
'up-to-date.')
"--force-reinstall",
dest="force_reinstall",
action="store_true",
help="Reinstall all packages even if they are already up-to-date.",
)
self.cmd_opts.add_option(
'-I', '--ignore-installed',
dest='ignore_installed',
action='store_true',
help='Ignore the installed packages, overwriting them. '
'This can break your system if the existing package '
'is of a different version or was installed '
'with a different package manager!'
"-I",
"--ignore-installed",
dest="ignore_installed",
action="store_true",
help=(
"Ignore the installed packages, overwriting them. "
"This can break your system if the existing package "
"is of a different version or was installed "
"with a different package manager!"
),
)
self.cmd_opts.add_option(cmdoptions.ignore_requires_python())
@@ -249,11 +265,14 @@ class InstallCommand(RequirementCommand):
if options.target_dir:
options.ignore_installed = True
options.target_dir = os.path.abspath(options.target_dir)
if (os.path.exists(options.target_dir) and not
os.path.isdir(options.target_dir)):
if (
# fmt: off
os.path.exists(options.target_dir) and
not os.path.isdir(options.target_dir)
# fmt: on
):
raise CommandError(
"Target path exists but is not a directory, will not "
"continue."
"Target path exists but is not a directory, will not continue."
)
# Create a target directory for using with the target option
@@ -285,9 +304,13 @@ class InstallCommand(RequirementCommand):
try:
reqs = self.get_requirements(args, options, finder, session)
reject_location_related_install_options(
reqs, options.install_options
)
# Only when installing is it permitted to use PEP 660.
# In other circumstances (pip wheel, pip download) we generate
# regular (i.e. non editable) metadata and wheels.
for req in reqs:
req.permit_editable_wheels = True
reject_location_related_install_options(reqs, options.install_options)
preparer = self.make_requirement_preparer(
temp_build_dir=directory,
@@ -296,6 +319,7 @@ class InstallCommand(RequirementCommand):
session=session,
finder=finder,
use_user_site=options.use_user_site,
verbosity=self.verbosity,
)
resolver = self.make_resolver(
preparer=preparer,
@@ -324,19 +348,14 @@ class InstallCommand(RequirementCommand):
# If we're not replacing an already installed pip,
# we're not modifying it.
modifying_pip = pip_req.satisfied_by is None
protect_pip_from_modification_on_windows(
modifying_pip=modifying_pip
)
protect_pip_from_modification_on_windows(modifying_pip=modifying_pip)
check_binary_allowed = get_check_binary_allowed(
finder.format_control
)
check_binary_allowed = get_check_binary_allowed(finder.format_control)
reqs_to_build = [
r for r in requirement_set.requirements.values()
if should_build_for_install_command(
r, check_binary_allowed
)
r
for r in requirement_set.requirements.values()
if should_build_for_install_command(r, check_binary_allowed)
]
_, build_failures = build(
@@ -347,36 +366,32 @@ class InstallCommand(RequirementCommand):
global_options=[],
)
# If we're using PEP 517, we cannot do a direct install
# If we're using PEP 517, we cannot do a legacy setup.py install
# so we fail here.
pep517_build_failure_names: List[str] = [
r.name # type: ignore
for r in build_failures if r.use_pep517
r.name for r in build_failures if r.use_pep517 # type: ignore
]
if pep517_build_failure_names:
raise InstallationError(
"Could not build wheels for {} which use"
" PEP 517 and cannot be installed directly".format(
"Could not build wheels for {}, which is required to "
"install pyproject.toml-based projects".format(
", ".join(pep517_build_failure_names)
)
)
# For now, we just warn about failures building legacy
# requirements, as we'll fall through to a direct
# install for those.
# requirements, as we'll fall through to a setup.py install for
# those.
for r in build_failures:
if not r.use_pep517:
r.legacy_install_reason = 8368
to_install = resolver.get_installation_order(
requirement_set
)
to_install = resolver.get_installation_order(requirement_set)
# Check for conflicts in the package set we're installing.
conflicts: Optional[ConflictDetails] = None
should_warn_about_conflicts = (
not options.ignore_dependencies and
options.warn_about_conflicts
not options.ignore_dependencies and options.warn_about_conflicts
)
if should_warn_about_conflicts:
conflicts = self._determine_conflicts(to_install)
@@ -408,7 +423,7 @@ class InstallCommand(RequirementCommand):
)
env = get_environment(lib_locations)
installed.sort(key=operator.attrgetter('name'))
installed.sort(key=operator.attrgetter("name"))
items = []
for result in installed:
item = result.name
@@ -426,16 +441,19 @@ class InstallCommand(RequirementCommand):
resolver_variant=self.determine_resolver_variant(options),
)
installed_desc = ' '.join(items)
installed_desc = " ".join(items)
if installed_desc:
write_output(
'Successfully installed %s', installed_desc,
"Successfully installed %s",
installed_desc,
)
except OSError as error:
show_traceback = (self.verbosity >= 1)
show_traceback = self.verbosity >= 1
message = create_os_error_message(
error, show_traceback, options.use_user_site,
error,
show_traceback,
options.use_user_site,
)
logger.error(message, exc_info=show_traceback) # noqa
@@ -461,7 +479,7 @@ class InstallCommand(RequirementCommand):
# Checking both purelib and platlib directories for installed
# packages to be moved to target directory
scheme = get_scheme('', home=target_temp_dir.path)
scheme = get_scheme("", home=target_temp_dir.path)
purelib_dir = scheme.purelib
platlib_dir = scheme.platlib
data_dir = scheme.data
@@ -483,18 +501,18 @@ class InstallCommand(RequirementCommand):
if os.path.exists(target_item_dir):
if not upgrade:
logger.warning(
'Target directory %s already exists. Specify '
'--upgrade to force replacement.',
target_item_dir
"Target directory %s already exists. Specify "
"--upgrade to force replacement.",
target_item_dir,
)
continue
if os.path.islink(target_item_dir):
logger.warning(
'Target directory %s already exists and is '
'a link. pip will not automatically replace '
'links, please remove if replacement is '
'desired.',
target_item_dir
"Target directory %s already exists and is "
"a link. pip will not automatically replace "
"links, please remove if replacement is "
"desired.",
target_item_dir,
)
continue
if os.path.isdir(target_item_dir):
@@ -502,10 +520,7 @@ class InstallCommand(RequirementCommand):
else:
os.remove(target_item_dir)
shutil.move(
os.path.join(lib_dir, item),
target_item_dir
)
shutil.move(os.path.join(lib_dir, item), target_item_dir)
def _determine_conflicts(
self, to_install: List[InstallRequirement]
@@ -567,7 +582,7 @@ class InstallCommand(RequirementCommand):
requirement=req,
dep_name=dep_name,
dep_version=dep_version,
you=("you" if resolver_variant == "2020-resolver" else "you'll")
you=("you" if resolver_variant == "2020-resolver" else "you'll"),
)
parts.append(message)
@@ -575,14 +590,14 @@ class InstallCommand(RequirementCommand):
def get_lib_location_guesses(
user: bool = False,
home: Optional[str] = None,
root: Optional[str] = None,
isolated: bool = False,
prefix: Optional[str] = None
user: bool = False,
home: Optional[str] = None,
root: Optional[str] = None,
isolated: bool = False,
prefix: Optional[str] = None,
) -> List[str]:
scheme = get_scheme(
'',
"",
user=user,
home=home,
root=root,
@@ -594,8 +609,8 @@ def get_lib_location_guesses(
def site_packages_writable(root: Optional[str], isolated: bool) -> bool:
return all(
test_writable_dir(d) for d in set(
get_lib_location_guesses(root=root, isolated=isolated))
test_writable_dir(d)
for d in set(get_lib_location_guesses(root=root, isolated=isolated))
)
@@ -653,8 +668,10 @@ def decide_user_install(
logger.debug("Non-user install because site-packages writeable")
return False
logger.info("Defaulting to user installation because normal site-packages "
"is not writeable")
logger.info(
"Defaulting to user installation because normal site-packages "
"is not writeable"
)
return True
@@ -664,6 +681,7 @@ def reject_location_related_install_options(
"""If any location-changing --install-option arguments were passed for
requirements or on the command-line, then show a deprecation warning.
"""
def format_options(option_names: Iterable[str]) -> List[str]:
return ["--{}".format(name.replace("_", "-")) for name in option_names]
@@ -683,9 +701,7 @@ def reject_location_related_install_options(
location_options = parse_distutils_args(options)
if location_options:
offenders.append(
"{!r} from command line".format(
format_options(location_options.keys())
)
"{!r} from command line".format(format_options(location_options.keys()))
)
if not offenders:
@@ -694,9 +710,7 @@ def reject_location_related_install_options(
raise CommandError(
"Location-changing options found in --install-option: {}."
" This is unsupported, use pip-level options like --user,"
" --prefix, --root, and --target instead.".format(
"; ".join(offenders)
)
" --prefix, --root, and --target instead.".format("; ".join(offenders))
)
@@ -727,18 +741,25 @@ def create_os_error_message(
permissions_part = "Check the permissions"
if not running_under_virtualenv() and not using_user_site:
parts.extend([
user_option_part, " or ",
permissions_part.lower(),
])
parts.extend(
[
user_option_part,
" or ",
permissions_part.lower(),
]
)
else:
parts.append(permissions_part)
parts.append(".\n")
# Suggest the user to enable Long Paths if path length is
# more than 260
if (WINDOWS and error.errno == errno.ENOENT and error.filename and
len(error.filename) > 260):
if (
WINDOWS
and error.errno == errno.ENOENT
and error.filename
and len(error.filename) > 260
):
parts.append(
"HINT: This error might have occurred since "
"this system does not have Windows Long Path "
+106 -82
View File
@@ -14,8 +14,8 @@ from pipenv.patched.notpip._internal.index.package_finder import PackageFinder
from pipenv.patched.notpip._internal.metadata import BaseDistribution, get_environment
from pipenv.patched.notpip._internal.models.selection_prefs import SelectionPreferences
from pipenv.patched.notpip._internal.network.session import PipSession
from pipenv.patched.notpip._internal.utils.misc import stdlib_pkgs, tabulate, write_output
from pipenv.patched.notpip._internal.utils.parallel import map_multithread
from pipenv.patched.notpip._internal.utils.compat import stdlib_pkgs
from pipenv.patched.notpip._internal.utils.misc import tabulate, write_output
if TYPE_CHECKING:
from pipenv.patched.notpip._internal.metadata.base import DistributionVersion
@@ -26,6 +26,7 @@ if TYPE_CHECKING:
These will be populated during ``get_outdated()``. This is dirty but
makes the rest of the code much cleaner.
"""
latest_version: DistributionVersion
latest_filetype: str
@@ -48,77 +49,85 @@ class ListCommand(IndexGroupCommand):
def add_options(self) -> None:
self.cmd_opts.add_option(
'-o', '--outdated',
action='store_true',
"-o",
"--outdated",
action="store_true",
default=False,
help='List outdated packages')
self.cmd_opts.add_option(
'-u', '--uptodate',
action='store_true',
default=False,
help='List uptodate packages')
self.cmd_opts.add_option(
'-e', '--editable',
action='store_true',
default=False,
help='List editable projects.')
self.cmd_opts.add_option(
'-l', '--local',
action='store_true',
default=False,
help=('If in a virtualenv that has global access, do not list '
'globally-installed packages.'),
help="List outdated packages",
)
self.cmd_opts.add_option(
'--user',
dest='user',
action='store_true',
"-u",
"--uptodate",
action="store_true",
default=False,
help='Only output packages installed in user-site.')
help="List uptodate packages",
)
self.cmd_opts.add_option(
"-e",
"--editable",
action="store_true",
default=False,
help="List editable projects.",
)
self.cmd_opts.add_option(
"-l",
"--local",
action="store_true",
default=False,
help=(
"If in a virtualenv that has global access, do not list "
"globally-installed packages."
),
)
self.cmd_opts.add_option(
"--user",
dest="user",
action="store_true",
default=False,
help="Only output packages installed in user-site.",
)
self.cmd_opts.add_option(cmdoptions.list_path())
self.cmd_opts.add_option(
'--pre',
action='store_true',
"--pre",
action="store_true",
default=False,
help=("Include pre-release and development versions. By default, "
"pip only finds stable versions."),
help=(
"Include pre-release and development versions. By default, "
"pip only finds stable versions."
),
)
self.cmd_opts.add_option(
'--format',
action='store',
dest='list_format',
"--format",
action="store",
dest="list_format",
default="columns",
choices=('columns', 'freeze', 'json'),
help="Select the output format among: columns (default), freeze, "
"or json",
choices=("columns", "freeze", "json"),
help="Select the output format among: columns (default), freeze, or json",
)
self.cmd_opts.add_option(
'--not-required',
action='store_true',
dest='not_required',
help="List packages that are not dependencies of "
"installed packages.",
"--not-required",
action="store_true",
dest="not_required",
help="List packages that are not dependencies of installed packages.",
)
self.cmd_opts.add_option(
'--exclude-editable',
action='store_false',
dest='include_editable',
help='Exclude editable package from output.',
"--exclude-editable",
action="store_false",
dest="include_editable",
help="Exclude editable package from output.",
)
self.cmd_opts.add_option(
'--include-editable',
action='store_true',
dest='include_editable',
help='Include editable package from output.',
"--include-editable",
action="store_true",
dest="include_editable",
help="Include editable package from output.",
default=True,
)
self.cmd_opts.add_option(cmdoptions.list_exclude())
index_opts = cmdoptions.make_option_group(
cmdoptions.index_group, self.parser
)
index_opts = cmdoptions.make_option_group(cmdoptions.index_group, self.parser)
self.parser.insert_option_group(0, index_opts)
self.parser.insert_option_group(0, self.cmd_opts)
@@ -140,12 +149,12 @@ class ListCommand(IndexGroupCommand):
return PackageFinder.create(
link_collector=link_collector,
selection_prefs=selection_prefs,
use_deprecated_html5lib="html5lib" in options.deprecated_features_enabled,
)
def run(self, options: Values, args: List[str]) -> int:
if options.outdated and options.uptodate:
raise CommandError(
"Options --outdated and --uptodate cannot be combined.")
raise CommandError("Options --outdated and --uptodate cannot be combined.")
cmdoptions.check_list_path_option(options)
@@ -183,7 +192,8 @@ class ListCommand(IndexGroupCommand):
self, packages: "_ProcessedDists", options: Values
) -> "_ProcessedDists":
return [
dist for dist in self.iter_packages_latest_infos(packages, options)
dist
for dist in self.iter_packages_latest_infos(packages, options)
if dist.latest_version > dist.version
]
@@ -191,7 +201,8 @@ class ListCommand(IndexGroupCommand):
self, packages: "_ProcessedDists", options: Values
) -> "_ProcessedDists":
return [
dist for dist in self.iter_packages_latest_infos(packages, options)
dist
for dist in self.iter_packages_latest_infos(packages, options)
if dist.latest_version == dist.version
]
@@ -216,13 +227,16 @@ class ListCommand(IndexGroupCommand):
finder = self._build_package_finder(options, session)
def latest_info(
dist: "_DistWithLatestInfo"
dist: "_DistWithLatestInfo",
) -> Optional["_DistWithLatestInfo"]:
all_candidates = finder.find_all_candidates(dist.canonical_name)
if not options.pre:
# Remove prereleases
all_candidates = [candidate for candidate in all_candidates
if not candidate.version.is_prerelease]
all_candidates = [
candidate
for candidate in all_candidates
if not candidate.version.is_prerelease
]
evaluator = finder.make_candidate_evaluator(
project_name=dist.canonical_name,
@@ -233,14 +247,14 @@ class ListCommand(IndexGroupCommand):
remote_version = best_candidate.version
if best_candidate.link.is_wheel:
typ = 'wheel'
typ = "wheel"
else:
typ = 'sdist'
typ = "sdist"
dist.latest_version = remote_version
dist.latest_filetype = typ
return dist
for dist in map_multithread(latest_info, packages):
for dist in map(latest_info, packages):
if dist is not None:
yield dist
@@ -251,17 +265,18 @@ class ListCommand(IndexGroupCommand):
packages,
key=lambda dist: dist.canonical_name,
)
if options.list_format == 'columns' and packages:
if options.list_format == "columns" and packages:
data, header = format_for_columns(packages, options)
self.output_package_listing_columns(data, header)
elif options.list_format == 'freeze':
elif options.list_format == "freeze":
for dist in packages:
if options.verbose >= 1:
write_output("%s==%s (%s)", dist.raw_name,
dist.version, dist.location)
write_output(
"%s==%s (%s)", dist.raw_name, dist.version, dist.location
)
else:
write_output("%s==%s", dist.raw_name, dist.version)
elif options.list_format == 'json':
elif options.list_format == "json":
write_output(format_for_json(packages, options))
def output_package_listing_columns(
@@ -275,7 +290,7 @@ class ListCommand(IndexGroupCommand):
# Create and add a separator.
if len(data) > 0:
pkg_strings.insert(1, " ".join(map(lambda x: '-' * x, sizes)))
pkg_strings.insert(1, " ".join(map(lambda x: "-" * x, sizes)))
for val in pkg_strings:
write_output(val)
@@ -288,19 +303,22 @@ def format_for_columns(
Convert the package data into something usable
by output_package_listing_columns.
"""
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 = ["Package", "Version"]
data = []
if options.verbose >= 1 or any(x.editable for x in pkgs):
running_outdated = options.outdated
if running_outdated:
header.extend(["Latest", "Type"])
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
@@ -310,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)
@@ -324,14 +345,17 @@ def format_for_json(packages: "_ProcessedDists", options: Values) -> str:
data = []
for dist in packages:
info = {
'name': dist.raw_name,
'version': str(dist.version),
"name": dist.raw_name,
"version": str(dist.version),
}
if options.verbose >= 1:
info['location'] = dist.location or ""
info['installer'] = dist.installer
info["location"] = dist.location or ""
info["installer"] = dist.installer
if options.outdated:
info['latest_version'] = str(dist.latest_version)
info['latest_filetype'] = dist.latest_filetype
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)
@@ -27,6 +27,7 @@ if TYPE_CHECKING:
summary: str
versions: List[str]
logger = logging.getLogger(__name__)
@@ -39,17 +40,19 @@ class SearchCommand(Command, SessionCommandMixin):
def add_options(self) -> None:
self.cmd_opts.add_option(
'-i', '--index',
dest='index',
metavar='URL',
"-i",
"--index",
dest="index",
metavar="URL",
default=PyPI.pypi_url,
help='Base URL of Python Package Index (default %default)')
help="Base URL of Python Package Index (default %default)",
)
self.parser.insert_option_group(0, self.cmd_opts)
def run(self, options: Values, args: List[str]) -> int:
if not args:
raise CommandError('Missing required argument (search query).')
raise CommandError("Missing required argument (search query).")
query = args
pypi_hits = self.search(query, options)
hits = transform_hits(pypi_hits)
@@ -71,7 +74,7 @@ class SearchCommand(Command, SessionCommandMixin):
transport = PipXmlrpcTransport(index_url, session)
pypi = xmlrpc.client.ServerProxy(index_url, transport)
try:
hits = pypi.search({'name': query, 'summary': query}, 'or')
hits = pypi.search({"name": query, "summary": query}, "or")
except xmlrpc.client.Fault as fault:
message = "XMLRPC request failed [code: {code}]\n{string}".format(
code=fault.faultCode,
@@ -90,22 +93,22 @@ def transform_hits(hits: List[Dict[str, str]]) -> List["TransformedHit"]:
"""
packages: Dict[str, "TransformedHit"] = OrderedDict()
for hit in hits:
name = hit['name']
summary = hit['summary']
version = hit['version']
name = hit["name"]
summary = hit["summary"]
version = hit["version"]
if name not in packages.keys():
packages[name] = {
'name': name,
'summary': summary,
'versions': [version],
"name": name,
"summary": summary,
"versions": [version],
}
else:
packages[name]['versions'].append(version)
packages[name]["versions"].append(version)
# if this is the highest version, replace summary and score
if version == highest_version(packages[name]['versions']):
packages[name]['summary'] = summary
if version == highest_version(packages[name]["versions"]):
packages[name]["summary"] = summary
return list(packages.values())
@@ -116,14 +119,17 @@ def print_dist_installation_info(name: str, latest: str) -> None:
if dist is not None:
with indent_log():
if dist.version == latest:
write_output('INSTALLED: %s (latest)', dist.version)
write_output("INSTALLED: %s (latest)", dist.version)
else:
write_output('INSTALLED: %s', dist.version)
write_output("INSTALLED: %s", dist.version)
if parse_version(latest).pre:
write_output('LATEST: %s (pre-release; install'
' with "pip install --pre")', latest)
write_output(
"LATEST: %s (pre-release; install"
" with `pip install --pre`)",
latest,
)
else:
write_output('LATEST: %s', latest)
write_output("LATEST: %s", latest)
def print_results(
@@ -134,25 +140,29 @@ def print_results(
if not hits:
return
if name_column_width is None:
name_column_width = max([
len(hit['name']) + len(highest_version(hit.get('versions', ['-'])))
for hit in hits
]) + 4
name_column_width = (
max(
[
len(hit["name"]) + len(highest_version(hit.get("versions", ["-"])))
for hit in hits
]
)
+ 4
)
for hit in hits:
name = hit['name']
summary = hit['summary'] or ''
latest = highest_version(hit.get('versions', ['-']))
name = hit["name"]
summary = hit["summary"] or ""
latest = highest_version(hit.get("versions", ["-"]))
if terminal_width is not None:
target_width = terminal_width - name_column_width - 5
if target_width > 10:
# wrap and indent summary to fit terminal
summary_lines = textwrap.wrap(summary, target_width)
summary = ('\n' + ' ' * (name_column_width + 3)).join(
summary_lines)
summary = ("\n" + " " * (name_column_width + 3)).join(summary_lines)
name_latest = f'{name} ({latest})'
line = f'{name_latest:{name_column_width}} - {summary}'
name_latest = f"{name} ({latest})"
line = f"{name_latest:{name_column_width}} - {summary}"
try:
write_output(line)
print_dist_installation_info(name, latest)
@@ -1,8 +1,6 @@
import csv
import logging
import pathlib
from optparse import Values
from typing import Iterator, List, NamedTuple, Optional, Tuple
from typing import Iterator, List, NamedTuple, Optional
from pipenv.patched.notpip._vendor.packaging.utils import canonicalize_name
@@ -27,23 +25,26 @@ class ShowCommand(Command):
def add_options(self) -> None:
self.cmd_opts.add_option(
'-f', '--files',
dest='files',
action='store_true',
"-f",
"--files",
dest="files",
action="store_true",
default=False,
help='Show the full list of installed files for each package.')
help="Show the full list of installed files for each package.",
)
self.parser.insert_option_group(0, self.cmd_opts)
def run(self, options: Values, args: List[str]) -> int:
if not args:
logger.warning('ERROR: Please provide a package name or names.')
logger.warning("ERROR: Please provide a package name or names.")
return ERROR
query = args
results = search_packages_info(query)
if not print_results(
results, list_files=options.files, verbose=options.verbose):
results, list_files=options.files, verbose=options.verbose
):
return ERROR
return SUCCESS
@@ -66,33 +67,6 @@ class _PackageInfo(NamedTuple):
files: Optional[List[str]]
def _covert_legacy_entry(entry: Tuple[str, ...], info: Tuple[str, ...]) -> str:
"""Convert a legacy installed-files.txt path into modern RECORD path.
The legacy format stores paths relative to the info directory, while the
modern format stores paths relative to the package root, e.g. the
site-packages directory.
:param entry: Path parts of the installed-files.txt entry.
:param info: Path parts of the egg-info directory relative to package root.
:returns: The converted entry.
For best compatibility with symlinks, this does not use ``abspath()`` or
``Path.resolve()``, but tries to work with path parts:
1. While ``entry`` starts with ``..``, remove the equal amounts of parts
from ``info``; if ``info`` is empty, start appending ``..`` instead.
2. Join the two directly.
"""
while entry and entry[0] == "..":
if not info or info[-1] == "..":
info += ("..",)
else:
info = info[:-1]
entry = entry[1:]
return str(pathlib.Path(*info, *entry))
def search_packages_info(query: List[str]) -> Iterator[_PackageInfo]:
"""
Gather details from installed distributions. Print distribution name,
@@ -102,53 +76,20 @@ def search_packages_info(query: List[str]) -> Iterator[_PackageInfo]:
"""
env = get_default_environment()
installed = {
dist.canonical_name: dist
for dist in env.iter_distributions()
}
installed = {dist.canonical_name: dist for dist in env.iter_distributions()}
query_names = [canonicalize_name(name) for name in query]
missing = sorted(
[name for name, pkg in zip(query, query_names) if pkg not in installed]
)
if missing:
logger.warning('Package(s) not found: %s', ', '.join(missing))
logger.warning("Package(s) not found: %s", ", ".join(missing))
def _get_requiring_packages(current_dist: BaseDistribution) -> List[str]:
return [
def _get_requiring_packages(current_dist: BaseDistribution) -> Iterator[str]:
return (
dist.metadata["Name"] or "UNKNOWN"
for dist in installed.values()
if current_dist.canonical_name in {
canonicalize_name(d.name) for d in dist.iter_dependencies()
}
]
def _files_from_record(dist: BaseDistribution) -> Optional[Iterator[str]]:
try:
text = dist.read_text('RECORD')
except FileNotFoundError:
return None
# This extra Path-str cast normalizes entries.
return (str(pathlib.Path(row[0])) for row in csv.reader(text.splitlines()))
def _files_from_legacy(dist: BaseDistribution) -> Optional[Iterator[str]]:
try:
text = dist.read_text('installed-files.txt')
except FileNotFoundError:
return None
paths = (p for p in text.splitlines(keepends=False) if p)
root = dist.location
info = dist.info_directory
if root is None or info is None:
return paths
try:
info_rel = pathlib.Path(info).relative_to(root)
except ValueError: # info is not relative to root.
return paths
if not info_rel.parts: # info *is* root.
return paths
return (
_covert_legacy_entry(pathlib.Path(p).parts, info_rel.parts)
for p in paths
if current_dist.canonical_name
in {canonicalize_name(d.name) for d in dist.iter_dependencies()}
)
for query_name in query_names:
@@ -157,13 +98,16 @@ def search_packages_info(query: List[str]) -> Iterator[_PackageInfo]:
except KeyError:
continue
requires = sorted((req.name for req in dist.iter_dependencies()), key=str.lower)
required_by = sorted(_get_requiring_packages(dist), key=str.lower)
try:
entry_points_text = dist.read_text('entry_points.txt')
entry_points_text = dist.read_text("entry_points.txt")
entry_points = entry_points_text.splitlines(keepends=False)
except FileNotFoundError:
entry_points = []
files_iter = _files_from_record(dist) or _files_from_legacy(dist)
files_iter = dist.iter_declared_entries()
if files_iter is None:
files: Optional[List[str]] = None
else:
@@ -175,8 +119,8 @@ def search_packages_info(query: List[str]) -> Iterator[_PackageInfo]:
name=dist.raw_name,
version=str(dist.version),
location=dist.location or "",
requires=[req.name for req in dist.iter_dependencies()],
required_by=_get_requiring_packages(dist),
requires=requires,
required_by=required_by,
installer=dist.installer,
metadata_version=dist.metadata_version or "",
classifiers=metadata.get_all("Classifier", []),
@@ -212,8 +156,8 @@ def print_results(
write_output("Author-email: %s", dist.author_email)
write_output("License: %s", dist.license)
write_output("Location: %s", dist.location)
write_output("Requires: %s", ', '.join(dist.requires))
write_output("Required-by: %s", ', '.join(dist.required_by))
write_output("Requires: %s", ", ".join(dist.requires))
write_output("Required-by: %s", ", ".join(dist.required_by))
if verbose:
write_output("Metadata-Version: %s", dist.metadata_version)
@@ -35,19 +35,24 @@ class UninstallCommand(Command, SessionCommandMixin):
def add_options(self) -> None:
self.cmd_opts.add_option(
'-r', '--requirement',
dest='requirements',
action='append',
"-r",
"--requirement",
dest="requirements",
action="append",
default=[],
metavar='file',
help='Uninstall all the packages listed in the given requirements '
'file. This option can be used multiple times.',
metavar="file",
help=(
"Uninstall all the packages listed in the given requirements "
"file. This option can be used multiple times."
),
)
self.cmd_opts.add_option(
'-y', '--yes',
dest='yes',
action='store_true',
help="Don't ask for confirmation of uninstall deletions.")
"-y",
"--yes",
dest="yes",
action="store_true",
help="Don't ask for confirmation of uninstall deletions.",
)
self.parser.insert_option_group(0, self.cmd_opts)
@@ -57,7 +62,8 @@ class UninstallCommand(Command, SessionCommandMixin):
reqs_to_uninstall = {}
for name in args:
req = install_req_from_line(
name, isolated=options.isolated_mode,
name,
isolated=options.isolated_mode,
)
if req.name:
reqs_to_uninstall[canonicalize_name(req.name)] = req
@@ -70,18 +76,16 @@ class UninstallCommand(Command, SessionCommandMixin):
)
for filename in options.requirements:
for parsed_req in parse_requirements(
filename,
options=options,
session=session):
filename, options=options, session=session
):
req = install_req_from_parsed_requirement(
parsed_req,
isolated=options.isolated_mode
parsed_req, isolated=options.isolated_mode
)
if req.name:
reqs_to_uninstall[canonicalize_name(req.name)] = req
if not reqs_to_uninstall:
raise InstallationError(
f'You must give at least one requirement to {self.name} (see '
f"You must give at least one requirement to {self.name} (see "
f'"pip help {self.name}")'
)
@@ -91,7 +95,8 @@ class UninstallCommand(Command, SessionCommandMixin):
for req in reqs_to_uninstall.values():
uninstall_pathset = req.uninstall(
auto_confirm=options.yes, verbose=self.verbosity > 0,
auto_confirm=options.yes,
verbose=self.verbosity > 0,
)
if uninstall_pathset:
uninstall_pathset.commit()
@@ -43,12 +43,15 @@ class WheelCommand(RequirementCommand):
def add_options(self) -> None:
self.cmd_opts.add_option(
'-w', '--wheel-dir',
dest='wheel_dir',
metavar='dir',
"-w",
"--wheel-dir",
dest="wheel_dir",
metavar="dir",
default=os.curdir,
help=("Build wheels into <dir>, where the default is the "
"current working directory."),
help=(
"Build wheels into <dir>, where the default is the "
"current working directory."
),
)
self.cmd_opts.add_option(cmdoptions.no_binary())
self.cmd_opts.add_option(cmdoptions.only_binary())
@@ -62,13 +65,12 @@ class WheelCommand(RequirementCommand):
self.cmd_opts.add_option(cmdoptions.src())
self.cmd_opts.add_option(cmdoptions.ignore_requires_python())
self.cmd_opts.add_option(cmdoptions.no_deps())
self.cmd_opts.add_option(cmdoptions.build_dir())
self.cmd_opts.add_option(cmdoptions.progress_bar())
self.cmd_opts.add_option(
'--no-verify',
dest='no_verify',
action='store_true',
"--no-verify",
dest="no_verify",
action="store_true",
default=False,
help="Don't verify if built wheel is valid.",
)
@@ -77,11 +79,13 @@ class WheelCommand(RequirementCommand):
self.cmd_opts.add_option(cmdoptions.global_options())
self.cmd_opts.add_option(
'--pre',
action='store_true',
"--pre",
action="store_true",
default=False,
help=("Include pre-release and development versions. By default, "
"pip only finds stable versions."),
help=(
"Include pre-release and development versions. By default, "
"pip only finds stable versions."
),
)
self.cmd_opts.add_option(cmdoptions.require_hashes())
@@ -124,6 +128,7 @@ class WheelCommand(RequirementCommand):
finder=finder,
download_dir=options.wheel_dir,
use_user_site=False,
verbosity=self.verbosity,
)
resolver = self.make_resolver(
@@ -137,9 +142,7 @@ class WheelCommand(RequirementCommand):
self.trace_basic_info(finder)
requirement_set = resolver.resolve(
reqs, check_supported_wheels=True
)
requirement_set = resolver.resolve(reqs, check_supported_wheels=True)
reqs_to_build: List[InstallRequirement] = []
for req in requirement_set.requirements.values():
@@ -165,12 +168,11 @@ class WheelCommand(RequirementCommand):
except OSError as e:
logger.warning(
"Building wheel for %s failed: %s",
req.name, e,
req.name,
e,
)
build_failures.append(req)
if len(build_failures) != 0:
raise CommandError(
"Failed to build one or more wheels"
)
raise CommandError("Failed to build one or more wheels")
return SUCCESS
+63 -100
View File
@@ -13,7 +13,6 @@ Some terminology:
import configparser
import locale
import logging
import os
import sys
from typing import Any, Dict, Iterable, List, NewType, Optional, Tuple
@@ -24,41 +23,39 @@ from pipenv.patched.notpip._internal.exceptions import (
)
from pipenv.patched.notpip._internal.utils import appdirs
from pipenv.patched.notpip._internal.utils.compat import WINDOWS
from pipenv.patched.notpip._internal.utils.logging import getLogger
from pipenv.patched.notpip._internal.utils.misc import ensure_dir, enum
RawConfigParser = configparser.RawConfigParser # Shorthand
Kind = NewType("Kind", str)
CONFIG_BASENAME = 'pip.ini' if WINDOWS else 'pip.conf'
CONFIG_BASENAME = "pip.ini" if WINDOWS else "pip.conf"
ENV_NAMES_IGNORED = "version", "help"
# The kinds of configurations there are.
kinds = enum(
USER="user", # User Specific
GLOBAL="global", # System Wide
SITE="site", # [Virtual] Environment Specific
ENV="env", # from PIP_CONFIG_FILE
USER="user", # User Specific
GLOBAL="global", # System Wide
SITE="site", # [Virtual] Environment Specific
ENV="env", # from PIP_CONFIG_FILE
ENV_VAR="env-var", # from Environment Variables
)
OVERRIDE_ORDER = kinds.GLOBAL, kinds.USER, kinds.SITE, kinds.ENV, kinds.ENV_VAR
VALID_LOAD_ONLY = kinds.USER, kinds.GLOBAL, kinds.SITE
logger = logging.getLogger(__name__)
logger = getLogger(__name__)
# NOTE: Maybe use the optionx attribute to normalize keynames.
def _normalize_name(name):
# type: (str) -> str
"""Make a name consistent regardless of source (environment or file)
"""
name = name.lower().replace('_', '-')
if name.startswith('--'):
def _normalize_name(name: str) -> str:
"""Make a name consistent regardless of source (environment or file)"""
name = name.lower().replace("_", "-")
if name.startswith("--"):
name = name[2:] # only prefer long opts
return name
def _disassemble_key(name):
# type: (str) -> List[str]
def _disassemble_key(name: str) -> List[str]:
if "." not in name:
error_message = (
"Key does not contain dot separated section and key. "
@@ -68,22 +65,18 @@ def _disassemble_key(name):
return name.split(".", 1)
def get_configuration_files():
# type: () -> Dict[Kind, List[str]]
def get_configuration_files() -> Dict[Kind, List[str]]:
global_config_files = [
os.path.join(path, CONFIG_BASENAME)
for path in appdirs.site_config_dirs('pip')
os.path.join(path, CONFIG_BASENAME) for path in appdirs.site_config_dirs("pip")
]
site_config_file = os.path.join(sys.prefix, CONFIG_BASENAME)
legacy_config_file = os.path.join(
os.path.expanduser('~'),
'pip' if WINDOWS else '.pip',
os.path.expanduser("~"),
"pip" if WINDOWS else ".pip",
CONFIG_BASENAME,
)
new_config_file = os.path.join(
appdirs.user_config_dir("pip"), CONFIG_BASENAME
)
new_config_file = os.path.join(appdirs.user_config_dir("pip"), CONFIG_BASENAME)
return {
kinds.GLOBAL: global_config_files,
kinds.SITE: [site_config_file],
@@ -105,8 +98,7 @@ class Configuration:
and the data stored is also nice.
"""
def __init__(self, isolated, load_only=None):
# type: (bool, Optional[Kind]) -> None
def __init__(self, isolated: bool, load_only: Optional[Kind] = None) -> None:
super().__init__()
if load_only is not None and load_only not in VALID_LOAD_ONLY:
@@ -119,54 +111,44 @@ class Configuration:
self.load_only = load_only
# Because we keep track of where we got the data from
self._parsers = {
self._parsers: Dict[Kind, List[Tuple[str, RawConfigParser]]] = {
variant: [] for variant in OVERRIDE_ORDER
} # type: Dict[Kind, List[Tuple[str, RawConfigParser]]]
self._config = {
}
self._config: Dict[Kind, Dict[str, Any]] = {
variant: {} for variant in OVERRIDE_ORDER
} # type: Dict[Kind, Dict[str, Any]]
self._modified_parsers = [] # type: List[Tuple[str, RawConfigParser]]
}
self._modified_parsers: List[Tuple[str, RawConfigParser]] = []
def load(self):
# type: () -> None
"""Loads configuration from configuration files and environment
"""
def load(self) -> None:
"""Loads configuration from configuration files and environment"""
self._load_config_files()
if not self.isolated:
self._load_environment_vars()
def get_file_to_edit(self):
# type: () -> Optional[str]
"""Returns the file with highest priority in configuration
"""
assert self.load_only is not None, \
"Need to be specified a file to be editing"
def get_file_to_edit(self) -> Optional[str]:
"""Returns the file with highest priority in configuration"""
assert self.load_only is not None, "Need to be specified a file to be editing"
try:
return self._get_parser_to_modify()[0]
except IndexError:
return None
def items(self):
# type: () -> Iterable[Tuple[str, Any]]
def items(self) -> Iterable[Tuple[str, Any]]:
"""Returns key-value pairs like dict.items() representing the loaded
configuration
"""
return self._dictionary.items()
def get_value(self, key):
# type: (str) -> Any
"""Get a value from the configuration.
"""
def get_value(self, key: str) -> Any:
"""Get a value from the configuration."""
try:
return self._dictionary[key]
except KeyError:
raise ConfigurationError(f"No such key - {key}")
def set_value(self, key, value):
# type: (str, Any) -> None
"""Modify a value in the configuration.
"""
def set_value(self, key: str, value: Any) -> None:
"""Modify a value in the configuration."""
self._ensure_have_load_only()
assert self.load_only
@@ -183,8 +165,7 @@ class Configuration:
self._config[self.load_only][key] = value
self._mark_as_modified(fname, parser)
def unset_value(self, key):
# type: (str) -> None
def unset_value(self, key: str) -> None:
"""Unset a value in the configuration."""
self._ensure_have_load_only()
@@ -196,8 +177,9 @@ class Configuration:
if parser is not None:
section, name = _disassemble_key(key)
if not (parser.has_section(section)
and parser.remove_option(section, name)):
if not (
parser.has_section(section) and parser.remove_option(section, name)
):
# The option was not removed.
raise ConfigurationError(
"Fatal Internal error [id=1]. Please report as a bug."
@@ -210,10 +192,8 @@ class Configuration:
del self._config[self.load_only][key]
def save(self):
# type: () -> None
"""Save the current in-memory state.
"""
def save(self) -> None:
"""Save the current in-memory state."""
self._ensure_have_load_only()
for fname, parser in self._modified_parsers:
@@ -229,17 +209,14 @@ class Configuration:
# Private routines
#
def _ensure_have_load_only(self):
# type: () -> None
def _ensure_have_load_only(self) -> None:
if self.load_only is None:
raise ConfigurationError("Needed a specific file to be modifying.")
logger.debug("Will be working with %s variant only", self.load_only)
@property
def _dictionary(self):
# type: () -> Dict[str, Any]
"""A dictionary representing the loaded configuration.
"""
def _dictionary(self) -> Dict[str, Any]:
"""A dictionary representing the loaded configuration."""
# NOTE: Dictionaries are not populated if not loaded. So, conditionals
# are not needed here.
retval = {}
@@ -249,10 +226,8 @@ class Configuration:
return retval
def _load_config_files(self):
# type: () -> None
"""Loads configuration from configuration files
"""
def _load_config_files(self) -> None:
"""Loads configuration from configuration files"""
config_files = dict(self.iter_config_files())
if config_files[kinds.ENV][0:1] == [os.devnull]:
logger.debug(
@@ -266,9 +241,7 @@ class Configuration:
# If there's specific variant set in `load_only`, load only
# that variant, not the others.
if self.load_only is not None and variant != self.load_only:
logger.debug(
"Skipping file '%s' (variant: %s)", fname, variant
)
logger.debug("Skipping file '%s' (variant: %s)", fname, variant)
continue
parser = self._load_file(variant, fname)
@@ -276,9 +249,8 @@ class Configuration:
# Keeping track of the parsers used
self._parsers[variant].append((fname, parser))
def _load_file(self, variant, fname):
# type: (Kind, str) -> RawConfigParser
logger.debug("For variant '%s', will try loading '%s'", variant, fname)
def _load_file(self, variant: Kind, fname: str) -> RawConfigParser:
logger.verbose("For variant '%s', will try loading '%s'", variant, fname)
parser = self._construct_parser(fname)
for section in parser.sections():
@@ -287,22 +259,20 @@ class Configuration:
return parser
def _construct_parser(self, fname):
# type: (str) -> RawConfigParser
def _construct_parser(self, fname: str) -> RawConfigParser:
parser = configparser.RawConfigParser()
# If there is no such file, don't bother reading it but create the
# parser anyway, to hold the data.
# Doing this is useful when modifying and saving files, where we don't
# need to construct a parser.
if os.path.exists(fname):
locale_encoding = locale.getpreferredencoding(False)
try:
parser.read(fname)
parser.read(fname, encoding=locale_encoding)
except UnicodeDecodeError:
# See https://github.com/pypa/pip/issues/4963
raise ConfigurationFileCouldNotBeLoaded(
reason="contains invalid {} characters".format(
locale.getpreferredencoding(False)
),
reason=f"contains invalid {locale_encoding} characters",
fname=fname,
)
except configparser.Error as error:
@@ -310,16 +280,15 @@ class Configuration:
raise ConfigurationFileCouldNotBeLoaded(error=error)
return parser
def _load_environment_vars(self):
# type: () -> None
"""Loads configuration from environment variables
"""
def _load_environment_vars(self) -> None:
"""Loads configuration from environment variables"""
self._config[kinds.ENV_VAR].update(
self._normalized_keys(":env:", self.get_environ_vars())
)
def _normalized_keys(self, section, items):
# type: (str, Iterable[Tuple[str, Any]]) -> Dict[str, Any]
def _normalized_keys(
self, section: str, items: Iterable[Tuple[str, Any]]
) -> Dict[str, Any]:
"""Normalizes items to construct a dictionary with normalized keys.
This routine is where the names become keys and are made the same
@@ -331,8 +300,7 @@ class Configuration:
normalized[key] = val
return normalized
def get_environ_vars(self):
# type: () -> Iterable[Tuple[str, str]]
def get_environ_vars(self) -> Iterable[Tuple[str, str]]:
"""Returns a generator with all environmental vars with prefix PIP_"""
for key, val in os.environ.items():
if key.startswith("PIP_"):
@@ -341,8 +309,7 @@ class Configuration:
yield name, val
# XXX: This is patched in the tests.
def iter_config_files(self):
# type: () -> Iterable[Tuple[Kind, List[str]]]
def iter_config_files(self) -> Iterable[Tuple[Kind, List[str]]]:
"""Yields variant and configuration files associated with it.
This should be treated like items of a dictionary.
@@ -350,7 +317,7 @@ class Configuration:
# SMELL: Move the conditions out of this function
# environment variables have the lowest priority
config_file = os.environ.get('PIP_CONFIG_FILE', None)
config_file = os.environ.get("PIP_CONFIG_FILE", None)
if config_file is not None:
yield kinds.ENV, [config_file]
else:
@@ -372,13 +339,11 @@ class Configuration:
# finally virtualenv configuration first trumping others
yield kinds.SITE, config_files[kinds.SITE]
def get_values_in_config(self, variant):
# type: (Kind) -> Dict[str, Any]
def get_values_in_config(self, variant: Kind) -> Dict[str, Any]:
"""Get values present in a config file"""
return self._config[variant]
def _get_parser_to_modify(self):
# type: () -> Tuple[str, RawConfigParser]
def _get_parser_to_modify(self) -> Tuple[str, RawConfigParser]:
# Determine which parser to modify
assert self.load_only
parsers = self._parsers[self.load_only]
@@ -392,12 +357,10 @@ class Configuration:
return parsers[-1]
# XXX: This is patched in the tests.
def _mark_as_modified(self, fname, parser):
# type: (str, RawConfigParser) -> None
def _mark_as_modified(self, fname: str, parser: RawConfigParser) -> None:
file_parser_tuple = (fname, parser)
if file_parser_tuple not in self._modified_parsers:
self._modified_parsers.append(file_parser_tuple)
def __repr__(self):
# type: () -> str
def __repr__(self) -> str:
return f"{self.__class__.__name__}({self._dictionary!r})"
@@ -1,9 +1,7 @@
import abc
from typing import Optional
from pipenv.patched.notpip._vendor.pkg_resources import Distribution
from pipenv.patched.notpip._internal.index.package_finder import PackageFinder
from pipenv.patched.notpip._internal.metadata.base import BaseDistribution
from pipenv.patched.notpip._internal.req import InstallRequirement
@@ -28,7 +26,7 @@ class AbstractDistribution(metaclass=abc.ABCMeta):
self.req = req
@abc.abstractmethod
def get_pkg_resources_distribution(self) -> Optional[Distribution]:
def get_metadata_distribution(self) -> BaseDistribution:
raise NotImplementedError()
@abc.abstractmethod
@@ -1,9 +1,6 @@
from typing import Optional
from pipenv.patched.notpip._vendor.pkg_resources import Distribution
from pipenv.patched.notpip._internal.distributions.base import AbstractDistribution
from pipenv.patched.notpip._internal.index.package_finder import PackageFinder
from pipenv.patched.notpip._internal.metadata import BaseDistribution
class InstalledDistribution(AbstractDistribution):
@@ -13,7 +10,8 @@ class InstalledDistribution(AbstractDistribution):
been computed.
"""
def get_pkg_resources_distribution(self) -> Optional[Distribution]:
def get_metadata_distribution(self) -> BaseDistribution:
assert self.req.satisfied_by is not None, "not actually installed"
return self.req.satisfied_by
def prepare_distribution_metadata(
@@ -1,12 +1,11 @@
import logging
from typing import Set, Tuple
from pipenv.patched.notpip._vendor.pkg_resources import Distribution
from typing import Iterable, Set, Tuple
from pipenv.patched.notpip._internal.build_env import BuildEnvironment
from pipenv.patched.notpip._internal.distributions.base import AbstractDistribution
from pipenv.patched.notpip._internal.exceptions import InstallationError
from pipenv.patched.notpip._internal.index.package_finder import PackageFinder
from pipenv.patched.notpip._internal.metadata import BaseDistribution
from pipenv.patched.notpip._internal.utils.subprocess import runner_with_spinner_message
logger = logging.getLogger(__name__)
@@ -19,7 +18,7 @@ class SourceDistribution(AbstractDistribution):
generated, either using PEP 517 or using the legacy `setup.py egg_info`.
"""
def get_pkg_resources_distribution(self) -> Distribution:
def get_metadata_distribution(self) -> BaseDistribution:
return self.req.get_dist()
def prepare_distribution_metadata(
@@ -31,28 +30,23 @@ class SourceDistribution(AbstractDistribution):
# Set up the build isolation, if this requirement should be isolated
should_isolate = self.req.use_pep517 and build_isolation
if should_isolate:
self._setup_isolation(finder)
# Setup an isolated environment and install the build backend static
# requirements in it.
self._prepare_build_backend(finder)
# Check that if the requirement is editable, it either supports PEP 660 or
# has a setup.py or a setup.cfg. This cannot be done earlier because we need
# to setup the build backend to verify it supports build_editable, nor can
# it be done later, because we want to avoid installing build requirements
# needlessly. Doing it here also works around setuptools generating
# UNKNOWN.egg-info when running get_requires_for_build_wheel on a directory
# without setup.py nor setup.cfg.
self.req.isolated_editable_sanity_check()
# Install the dynamic build requirements.
self._install_build_reqs(finder)
self.req.prepare_metadata()
def _setup_isolation(self, finder: PackageFinder) -> None:
def _raise_conflicts(
conflicting_with: str, conflicting_reqs: Set[Tuple[str, str]]
) -> None:
format_string = (
"Some build dependencies for {requirement} "
"conflict with {conflicting_with}: {description}."
)
error_message = format_string.format(
requirement=self.req,
conflicting_with=conflicting_with,
description=", ".join(
f"{installed} is incompatible with {wanted}"
for installed, wanted in sorted(conflicting)
),
)
raise InstallationError(error_message)
def _prepare_build_backend(self, finder: PackageFinder) -> None:
# Isolate in a BuildEnvironment and install the build-time
# requirements.
pyproject_requires = self.req.pyproject_requires
@@ -60,13 +54,13 @@ class SourceDistribution(AbstractDistribution):
self.req.build_env = BuildEnvironment()
self.req.build_env.install_requirements(
finder, pyproject_requires, "overlay", "Installing build dependencies"
finder, pyproject_requires, "overlay", kind="build dependencies"
)
conflicting, missing = self.req.build_env.check_requirements(
self.req.requirements_to_check
)
if conflicting:
_raise_conflicts("PEP 517/518 supported requirements", conflicting)
self._raise_conflicts("PEP 517/518 supported requirements", conflicting)
if missing:
logger.warning(
"Missing build requirements in pyproject.toml for %s.",
@@ -77,19 +71,57 @@ class SourceDistribution(AbstractDistribution):
"pip cannot fall back to setuptools without %s.",
" and ".join(map(repr, sorted(missing))),
)
# Install any extra build dependencies that the backend requests.
# This must be done in a second pass, as the pyproject.toml
# dependencies must be installed before we can call the backend.
def _get_build_requires_wheel(self) -> Iterable[str]:
with self.req.build_env:
runner = runner_with_spinner_message("Getting requirements to build wheel")
backend = self.req.pep517_backend
assert backend is not None
with backend.subprocess_runner(runner):
reqs = backend.get_requires_for_build_wheel()
return backend.get_requires_for_build_wheel()
conflicting, missing = self.req.build_env.check_requirements(reqs)
def _get_build_requires_editable(self) -> Iterable[str]:
with self.req.build_env:
runner = runner_with_spinner_message(
"Getting requirements to build editable"
)
backend = self.req.pep517_backend
assert backend is not None
with backend.subprocess_runner(runner):
return backend.get_requires_for_build_editable()
def _install_build_reqs(self, finder: PackageFinder) -> None:
# Install any extra build dependencies that the backend requests.
# This must be done in a second pass, as the pyproject.toml
# dependencies must be installed before we can call the backend.
if (
self.req.editable
and self.req.permit_editable_wheels
and self.req.supports_pyproject_editable()
):
build_reqs = self._get_build_requires_editable()
else:
build_reqs = self._get_build_requires_wheel()
conflicting, missing = self.req.build_env.check_requirements(build_reqs)
if conflicting:
_raise_conflicts("the backend dependencies", conflicting)
self._raise_conflicts("the backend dependencies", conflicting)
self.req.build_env.install_requirements(
finder, missing, "normal", "Installing backend dependencies"
finder, missing, "normal", kind="backend dependencies"
)
def _raise_conflicts(
self, conflicting_with: str, conflicting_reqs: Set[Tuple[str, str]]
) -> None:
format_string = (
"Some build dependencies for {requirement} "
"conflict with {conflicting_with}: {description}."
)
error_message = format_string.format(
requirement=self.req,
conflicting_with=conflicting_with,
description=", ".join(
f"{installed} is incompatible with {wanted}"
for installed, wanted in sorted(conflicting_reqs)
),
)
raise InstallationError(error_message)
@@ -1,10 +1,12 @@
from zipfile import ZipFile
from pipenv.patched.notpip._vendor.pkg_resources import Distribution
from pipenv.patched.notpip._vendor.packaging.utils import canonicalize_name
from pipenv.patched.notpip._internal.distributions.base import AbstractDistribution
from pipenv.patched.notpip._internal.index.package_finder import PackageFinder
from pipenv.patched.notpip._internal.utils.wheel import pkg_resources_distribution_for_wheel
from pipenv.patched.notpip._internal.metadata import (
BaseDistribution,
FilesystemWheel,
get_wheel_distribution,
)
class WheelDistribution(AbstractDistribution):
@@ -13,20 +15,15 @@ class WheelDistribution(AbstractDistribution):
This does not need any preparation as wheels can be directly unpacked.
"""
def get_pkg_resources_distribution(self) -> Distribution:
def get_metadata_distribution(self) -> BaseDistribution:
"""Loads the metadata from the wheel file into memory and returns a
Distribution that uses it, not relying on the wheel file or
requirement.
"""
# Set as part of preparation during download.
assert self.req.local_file_path
# Wheels are never unnamed.
assert self.req.name
with ZipFile(self.req.local_file_path, allowZip64=True) as z:
return pkg_resources_distribution_for_wheel(
z, self.req.name, self.req.local_file_path
)
assert self.req.local_file_path, "Set as part of preparation during download"
assert self.req.name, "Wheels are never unnamed"
wheel = FilesystemWheel(self.req.local_file_path)
return get_wheel_distribution(wheel, canonicalize_name(self.req.name))
def prepare_distribution_metadata(
self, finder: PackageFinder, build_isolation: bool
+384 -123
View File
@@ -1,22 +1,174 @@
"""Exceptions used throughout package"""
"""Exceptions used throughout package.
This module MUST NOT try to import from anything within `pipenv.patched.notpip._internal` to
operate. This is expected to be importable from any/all files within the
subpackage and, thus, should not depend on them.
"""
import configparser
import re
from itertools import chain, groupby, repeat
from typing import TYPE_CHECKING, Dict, List, Optional
from typing import TYPE_CHECKING, Dict, List, Optional, Union
from pipenv.patched.notpip._vendor.pkg_resources import Distribution
from pipenv.patched.notpip._vendor.requests.models import Request, Response
from pipenv.patched.notpip._vendor.rich.console import Console, ConsoleOptions, RenderResult
from pipenv.patched.notpip._vendor.rich.markup import escape
from pipenv.patched.notpip._vendor.rich.text import Text
if TYPE_CHECKING:
from hashlib import _Hash
from typing import Literal
from pipenv.patched.notpip._internal.metadata import BaseDistribution
from pipenv.patched.notpip._internal.req.req_install import InstallRequirement
#
# Scaffolding
#
def _is_kebab_case(s: str) -> bool:
return re.match(r"^[a-z]+(-[a-z]+)*$", s) is not None
def _prefix_with_indent(
s: Union[Text, str],
console: Console,
*,
prefix: str,
indent: str,
) -> Text:
if isinstance(s, Text):
text = s
else:
text = console.render_str(s)
return console.render_str(prefix, overflow="ignore") + console.render_str(
f"\n{indent}", overflow="ignore"
).join(text.split(allow_blank=True))
class PipError(Exception):
"""Base pip exception"""
"""The base pip error."""
class DiagnosticPipError(PipError):
"""An error, that presents diagnostic information to the user.
This contains a bunch of logic, to enable pretty presentation of our error
messages. Each error gets a unique reference. Each error can also include
additional context, a hint and/or a note -- which are presented with the
main error message in a consistent style.
This is adapted from the error output styling in `sphinx-theme-builder`.
"""
reference: str
def __init__(
self,
*,
kind: 'Literal["error", "warning"]' = "error",
reference: Optional[str] = None,
message: Union[str, Text],
context: Optional[Union[str, Text]],
hint_stmt: Optional[Union[str, Text]],
note_stmt: Optional[Union[str, Text]] = None,
link: Optional[str] = None,
) -> None:
# Ensure a proper reference is provided.
if reference is None:
assert hasattr(self, "reference"), "error reference not provided!"
reference = self.reference
assert _is_kebab_case(reference), "error reference must be kebab-case!"
self.kind = kind
self.reference = reference
self.message = message
self.context = context
self.note_stmt = note_stmt
self.hint_stmt = hint_stmt
self.link = link
super().__init__(f"<{self.__class__.__name__}: {self.reference}>")
def __repr__(self) -> str:
return (
f"<{self.__class__.__name__}("
f"reference={self.reference!r}, "
f"message={self.message!r}, "
f"context={self.context!r}, "
f"note_stmt={self.note_stmt!r}, "
f"hint_stmt={self.hint_stmt!r}"
")>"
)
def __rich_console__(
self,
console: Console,
options: ConsoleOptions,
) -> RenderResult:
colour = "red" if self.kind == "error" else "yellow"
yield f"[{colour} bold]{self.kind}[/]: [bold]{self.reference}[/]"
yield ""
if not options.ascii_only:
# Present the main message, with relevant context indented.
if self.context is not None:
yield _prefix_with_indent(
self.message,
console,
prefix=f"[{colour}]×[/] ",
indent=f"[{colour}]│[/] ",
)
yield _prefix_with_indent(
self.context,
console,
prefix=f"[{colour}]╰─>[/] ",
indent=f"[{colour}] [/] ",
)
else:
yield _prefix_with_indent(
self.message,
console,
prefix="[red]×[/] ",
indent=" ",
)
else:
yield self.message
if self.context is not None:
yield ""
yield self.context
if self.note_stmt is not None or self.hint_stmt is not None:
yield ""
if self.note_stmt is not None:
yield _prefix_with_indent(
self.note_stmt,
console,
prefix="[magenta bold]note[/]: ",
indent=" ",
)
if self.hint_stmt is not None:
yield _prefix_with_indent(
self.hint_stmt,
console,
prefix="[cyan bold]hint[/]: ",
indent=" ",
)
if self.link is not None:
yield ""
yield f"Link: {self.link}"
#
# Actual Errors
#
class ConfigurationError(PipError):
"""General exception in configuration"""
@@ -29,17 +181,54 @@ class UninstallationError(PipError):
"""General exception during uninstallation"""
class MissingPyProjectBuildRequires(DiagnosticPipError):
"""Raised when pyproject.toml has `build-system`, but no `build-system.requires`."""
reference = "missing-pyproject-build-system-requires"
def __init__(self, *, package: str) -> None:
super().__init__(
message=f"Can not process {escape(package)}",
context=Text(
"This package has an invalid pyproject.toml file.\n"
"The [build-system] table is missing the mandatory `requires` key."
),
note_stmt="This is an issue with the package mentioned above, not pip.",
hint_stmt=Text("See PEP 518 for the detailed specification."),
)
class InvalidPyProjectBuildRequires(DiagnosticPipError):
"""Raised when pyproject.toml an invalid `build-system.requires`."""
reference = "invalid-pyproject-build-system-requires"
def __init__(self, *, package: str, reason: str) -> None:
super().__init__(
message=f"Can not process {escape(package)}",
context=Text(
"This package has an invalid `build-system.requires` key in "
f"pyproject.toml.\n{reason}"
),
note_stmt="This is an issue with the package mentioned above, not pip.",
hint_stmt=Text("See PEP 518 for the detailed specification."),
)
class NoneMetadataError(PipError):
"""
Raised when accessing "METADATA" or "PKG-INFO" metadata for a
pipenv.patched.notpip._vendor.pkg_resources.Distribution object and
`dist.has_metadata('METADATA')` returns True but
`dist.get_metadata('METADATA')` returns None (and similarly for
"PKG-INFO").
"""Raised when accessing a Distribution's "METADATA" or "PKG-INFO".
This signifies an inconsistency, when the Distribution claims to have
the metadata file (if not, raise ``FileNotFoundError`` instead), but is
not actually able to produce its content. This may be due to permission
errors.
"""
def __init__(self, dist, metadata_name):
# type: (Distribution, str) -> None
def __init__(
self,
dist: "BaseDistribution",
metadata_name: str,
) -> None:
"""
:param dist: A Distribution object.
:param metadata_name: The name of the metadata being accessed
@@ -48,28 +237,24 @@ class NoneMetadataError(PipError):
self.dist = dist
self.metadata_name = metadata_name
def __str__(self):
# type: () -> str
def __str__(self) -> str:
# Use `dist` in the error message because its stringification
# includes more information, like the version and location.
return (
'None {} metadata found for distribution: {}'.format(
self.metadata_name, self.dist,
)
return "None {} metadata found for distribution: {}".format(
self.metadata_name,
self.dist,
)
class UserInstallationInvalid(InstallationError):
"""A --user install is requested on an environment without user site."""
def __str__(self):
# type: () -> str
def __str__(self) -> str:
return "User base directory is not specified"
class InvalidSchemeCombination(InstallationError):
def __str__(self):
# type: () -> str
def __str__(self) -> str:
before = ", ".join(str(a) for a in self.args[:-1])
return f"Cannot set {before} and {self.args[-1]} together"
@@ -102,8 +287,9 @@ class PreviousBuildDirError(PipError):
class NetworkConnectionError(PipError):
"""HTTP connection error"""
def __init__(self, error_msg, response=None, request=None):
# type: (str, Response, Request) -> None
def __init__(
self, error_msg: str, response: Response = None, request: Request = None
) -> None:
"""
Initialize NetworkConnectionError with `request` and `response`
objects.
@@ -111,13 +297,15 @@ class NetworkConnectionError(PipError):
self.response = response
self.request = request
self.error_msg = error_msg
if (self.response is not None and not self.request and
hasattr(response, 'request')):
if (
self.response is not None
and not self.request
and hasattr(response, "request")
):
self.request = self.response.request
super().__init__(error_msg, response, request)
def __str__(self):
# type: () -> str
def __str__(self) -> str:
return str(self.error_msg)
@@ -129,6 +317,17 @@ class UnsupportedWheel(InstallationError):
"""Unsupported wheel."""
class InvalidWheel(InstallationError):
"""Invalid (e.g. corrupt) wheel."""
def __init__(self, location: str, name: str):
self.location = location
self.name = name
def __str__(self) -> str:
return f"Wheel '{self.name}' located at {self.location} is invalid."
class MetadataInconsistent(InstallationError):
"""Built metadata contains inconsistent information.
@@ -136,15 +335,16 @@ class MetadataInconsistent(InstallationError):
that do not match the information previously obtained from sdist filename
or user-supplied ``#egg=`` value.
"""
def __init__(self, ireq, field, f_val, m_val):
# type: (InstallRequirement, str, str, str) -> None
def __init__(
self, ireq: "InstallRequirement", field: str, f_val: str, m_val: str
) -> None:
self.ireq = ireq
self.field = field
self.f_val = f_val
self.m_val = m_val
def __str__(self):
# type: () -> str
def __str__(self) -> str:
template = (
"Requested {} has inconsistent {}: "
"filename has {!r}, but metadata has {!r}"
@@ -152,51 +352,102 @@ class MetadataInconsistent(InstallationError):
return template.format(self.ireq, self.field, self.f_val, self.m_val)
class InstallationSubprocessError(InstallationError):
"""A subprocess call failed during installation."""
def __init__(self, returncode, description):
# type: (int, str) -> None
self.returncode = returncode
self.description = description
class LegacyInstallFailure(DiagnosticPipError):
"""Error occurred while executing `setup.py install`"""
def __str__(self):
# type: () -> str
return (
"Command errored out with exit status {}: {} "
"Check the logs for full command output."
).format(self.returncode, self.description)
reference = "legacy-install-failure"
def __init__(self, package_details: str) -> None:
super().__init__(
message="Encountered error while trying to install package.",
context=package_details,
hint_stmt="See above for output from the failure.",
note_stmt="This is an issue with the package mentioned above, not pip.",
)
class InstallationSubprocessError(DiagnosticPipError, InstallationError):
"""A subprocess call failed."""
reference = "subprocess-exited-with-error"
def __init__(
self,
*,
command_description: str,
exit_code: int,
output_lines: Optional[List[str]],
) -> None:
if output_lines is None:
output_prompt = Text("See above for output.")
else:
output_prompt = (
Text.from_markup(f"[red][{len(output_lines)} lines of output][/]\n")
+ Text("".join(output_lines))
+ Text.from_markup(R"[red]\[end of output][/]")
)
super().__init__(
message=(
f"[green]{escape(command_description)}[/] did not run successfully.\n"
f"exit code: {exit_code}"
),
context=output_prompt,
hint_stmt=None,
note_stmt=(
"This error originates from a subprocess, and is likely not a "
"problem with pip."
),
)
self.command_description = command_description
self.exit_code = exit_code
def __str__(self) -> str:
return f"{self.command_description} exited with {self.exit_code}"
class MetadataGenerationFailed(InstallationSubprocessError, InstallationError):
reference = "metadata-generation-failed"
def __init__(
self,
*,
package_details: str,
) -> None:
super(InstallationSubprocessError, self).__init__(
message="Encountered error while generating package metadata.",
context=escape(package_details),
hint_stmt="See above for details.",
note_stmt="This is an issue with the package mentioned above, not pip.",
)
def __str__(self) -> str:
return "metadata generation failed"
class HashErrors(InstallationError):
"""Multiple HashError instances rolled into one for reporting"""
def __init__(self):
# type: () -> None
self.errors = [] # type: List[HashError]
def __init__(self) -> None:
self.errors: List["HashError"] = []
def append(self, error):
# type: (HashError) -> None
def append(self, error: "HashError") -> None:
self.errors.append(error)
def __str__(self):
# type: () -> str
def __str__(self) -> str:
lines = []
self.errors.sort(key=lambda e: e.order)
for cls, errors_of_cls in groupby(self.errors, lambda e: e.__class__):
lines.append(cls.head)
lines.extend(e.body() for e in errors_of_cls)
if lines:
return '\n'.join(lines)
return ''
return "\n".join(lines)
return ""
def __nonzero__(self):
# type: () -> bool
def __bool__(self) -> bool:
return bool(self.errors)
def __bool__(self):
# type: () -> bool
return self.__nonzero__()
class HashError(InstallationError):
"""
@@ -214,12 +465,12 @@ class HashError(InstallationError):
typically available earlier.
"""
req = None # type: Optional[InstallRequirement]
head = ''
order = -1 # type: int
def body(self):
# type: () -> str
req: Optional["InstallRequirement"] = None
head = ""
order: int = -1
def body(self) -> str:
"""Return a summary of me for display under the heading.
This default implementation simply prints a description of the
@@ -229,21 +480,19 @@ class HashError(InstallationError):
its link already populated by the resolver's _populate_link().
"""
return f' {self._requirement_name()}'
return f" {self._requirement_name()}"
def __str__(self):
# type: () -> str
return f'{self.head}\n{self.body()}'
def __str__(self) -> str:
return f"{self.head}\n{self.body()}"
def _requirement_name(self):
# type: () -> str
def _requirement_name(self) -> str:
"""Return a description of the requirement that triggered me.
This default implementation returns long description of the req, with
line numbers
"""
return str(self.req) if self.req else 'unknown package'
return str(self.req) if self.req else "unknown package"
class VcsHashUnsupported(HashError):
@@ -251,8 +500,10 @@ class VcsHashUnsupported(HashError):
we don't have a method for hashing those."""
order = 0
head = ("Can't verify hashes for these requirements because we don't "
"have a way to hash version control repositories:")
head = (
"Can't verify hashes for these requirements because we don't "
"have a way to hash version control repositories:"
)
class DirectoryUrlHashUnsupported(HashError):
@@ -260,32 +511,34 @@ class DirectoryUrlHashUnsupported(HashError):
we don't have a method for hashing those."""
order = 1
head = ("Can't verify hashes for these file:// requirements because they "
"point to directories:")
head = (
"Can't verify hashes for these file:// requirements because they "
"point to directories:"
)
class HashMissing(HashError):
"""A hash was needed for a requirement but is absent."""
order = 2
head = ('Hashes are required in --require-hashes mode, but they are '
'missing from some requirements. Here is a list of those '
'requirements along with the hashes their downloaded archives '
'actually had. Add lines like these to your requirements files to '
'prevent tampering. (If you did not enable --require-hashes '
'manually, note that it turns on automatically when any package '
'has a hash.)')
head = (
"Hashes are required in --require-hashes mode, but they are "
"missing from some requirements. Here is a list of those "
"requirements along with the hashes their downloaded archives "
"actually had. Add lines like these to your requirements files to "
"prevent tampering. (If you did not enable --require-hashes "
"manually, note that it turns on automatically when any package "
"has a hash.)"
)
def __init__(self, gotten_hash):
# type: (str) -> None
def __init__(self, gotten_hash: str) -> None:
"""
:param gotten_hash: The hash of the (possibly malicious) archive we
just downloaded
"""
self.gotten_hash = gotten_hash
def body(self):
# type: () -> str
def body(self) -> str:
# Dodge circular import.
from pipenv.patched.notpip._internal.utils.hashes import FAVORITE_HASH
@@ -294,13 +547,16 @@ class HashMissing(HashError):
# In the case of URL-based requirements, display the original URL
# seen in the requirements file rather than the package name,
# so the output can be directly copied into the requirements file.
package = (self.req.original_link if self.req.original_link
# In case someone feeds something downright stupid
# to InstallRequirement's constructor.
else getattr(self.req, 'req', None))
return ' {} --hash={}:{}'.format(package or 'unknown package',
FAVORITE_HASH,
self.gotten_hash)
package = (
self.req.original_link
if self.req.original_link
# In case someone feeds something downright stupid
# to InstallRequirement's constructor.
else getattr(self.req, "req", None)
)
return " {} --hash={}:{}".format(
package or "unknown package", FAVORITE_HASH, self.gotten_hash
)
class HashUnpinned(HashError):
@@ -308,8 +564,10 @@ class HashUnpinned(HashError):
version."""
order = 3
head = ('In --require-hashes mode, all requirements must have their '
'versions pinned with ==. These do not:')
head = (
"In --require-hashes mode, all requirements must have their "
"versions pinned with ==. These do not:"
)
class HashMismatch(HashError):
@@ -321,14 +579,16 @@ class HashMismatch(HashError):
improve its error message.
"""
order = 4
head = ('THESE PACKAGES DO NOT MATCH THE HASHES FROM THE REQUIREMENTS '
'FILE. If you have updated the package versions, please update '
'the hashes. Otherwise, examine the package contents carefully; '
'someone may have tampered with them.')
def __init__(self, allowed, gots):
# type: (Dict[str, List[str]], Dict[str, _Hash]) -> None
order = 4
head = (
"THESE PACKAGES DO NOT MATCH THE HASHES FROM THE REQUIREMENTS "
"FILE. If you have updated the package versions, please update "
"the hashes. Otherwise, examine the package contents carefully; "
"someone may have tampered with them."
)
def __init__(self, allowed: Dict[str, List[str]], gots: Dict[str, "_Hash"]) -> None:
"""
:param allowed: A dict of algorithm names pointing to lists of allowed
hex digests
@@ -338,13 +598,10 @@ class HashMismatch(HashError):
self.allowed = allowed
self.gots = gots
def body(self):
# type: () -> str
return ' {}:\n{}'.format(self._requirement_name(),
self._hash_comparison())
def body(self) -> str:
return " {}:\n{}".format(self._requirement_name(), self._hash_comparison())
def _hash_comparison(self):
# type: () -> str
def _hash_comparison(self) -> str:
"""
Return a comparison of actual and expected hash values.
@@ -355,20 +612,22 @@ class HashMismatch(HashError):
Got bcdefbcdefbcdefbcdefbcdefbcdefbcdefbcdefbcdef
"""
def hash_then_or(hash_name):
# type: (str) -> chain[str]
def hash_then_or(hash_name: str) -> "chain[str]":
# For now, all the decent hashes have 6-char names, so we can get
# away with hard-coding space literals.
return chain([hash_name], repeat(' or'))
return chain([hash_name], repeat(" or"))
lines = [] # type: List[str]
lines: List[str] = []
for hash_name, expecteds in self.allowed.items():
prefix = hash_then_or(hash_name)
lines.extend((' Expected {} {}'.format(next(prefix), e))
for e in expecteds)
lines.append(' Got {}\n'.format(
self.gots[hash_name].hexdigest()))
return '\n'.join(lines)
lines.extend(
(" Expected {} {}".format(next(prefix), e)) for e in expecteds
)
lines.append(
" Got {}\n".format(self.gots[hash_name].hexdigest())
)
return "\n".join(lines)
class UnsupportedPythonVersion(InstallationError):
@@ -377,18 +636,20 @@ class UnsupportedPythonVersion(InstallationError):
class ConfigurationFileCouldNotBeLoaded(ConfigurationError):
"""When there are errors while loading a configuration file
"""
"""When there are errors while loading a configuration file"""
def __init__(self, reason="could not be loaded", fname=None, error=None):
# type: (str, Optional[str], Optional[configparser.Error]) -> None
def __init__(
self,
reason: str = "could not be loaded",
fname: Optional[str] = None,
error: Optional[configparser.Error] = None,
) -> None:
super().__init__(error)
self.reason = reason
self.fname = fname
self.error = error
def __str__(self):
# type: () -> str
def __str__(self) -> str:
if self.fname is not None:
message_part = f" in {self.fname}."
else:
@@ -5,7 +5,6 @@ The main purpose of this module is to expose LinkCollector.collect_sources().
import cgi
import collections
import functools
import html
import itertools
import logging
import os
@@ -13,16 +12,19 @@ import re
import urllib.parse
import urllib.request
import xml.etree.ElementTree
from html.parser import HTMLParser
from optparse import Values
from typing import (
TYPE_CHECKING,
Callable,
Iterable,
Dict,
Iterable,
List,
MutableMapping,
NamedTuple,
Optional,
Sequence,
Tuple,
Union,
)
@@ -41,6 +43,11 @@ from pipenv.patched.notpip._internal.vcs import vcs
from .sources import CandidatesFromPage, LinkSource, build_source
if TYPE_CHECKING:
from typing import Protocol
else:
Protocol = object
logger = logging.getLogger(__name__)
HTMLElement = xml.etree.ElementTree.Element
@@ -53,7 +60,7 @@ def _match_vcs_scheme(url: str) -> Optional[str]:
Returns the matched VCS scheme, or None if there's no match.
"""
for scheme in vcs.schemes:
if url.lower().startswith(scheme) and url[len(scheme)] in '+:':
if url.lower().startswith(scheme) and url[len(scheme)] in "+:":
return scheme
return None
@@ -86,7 +93,7 @@ def _ensure_html_response(url: str, session: PipSession) -> None:
`_NotHTML` if the content type is not text/html.
"""
scheme, netloc, path, query, fragment = urllib.parse.urlsplit(url)
if scheme not in {'http', 'https'}:
if scheme not in {"http", "https"}:
raise _NotHTTP()
resp = session.head(url, allow_redirects=True)
@@ -111,7 +118,7 @@ def _get_html_response(url: str, session: PipSession) -> Response:
if is_archive_file(Link(url).filename):
_ensure_html_response(url, session=session)
logger.debug('Getting page %s', redact_auth_from_url(url))
logger.debug("Getting page %s", redact_auth_from_url(url))
resp = session.get(
url,
@@ -146,12 +153,11 @@ def _get_html_response(url: str, session: PipSession) -> Response:
def _get_encoding_from_headers(headers: ResponseHeaders) -> Optional[str]:
"""Determine if we have any encoding information in our headers.
"""
"""Determine if we have any encoding information in our headers."""
if headers and "Content-Type" in headers:
content_type, params = cgi.parse_header(headers["Content-Type"])
if "charset" in params:
return params['charset']
return params["charset"]
return None
@@ -166,6 +172,8 @@ def _determine_base_url(document: HTMLElement, page_url: str) -> str:
:param document: An HTML document representation. The current
implementation expects the result of ``html5lib.parse()``.
:param page_url: The URL of the HTML document.
TODO: Remove when `html5lib` is dropped.
"""
for base in document.findall(".//base"):
href = base.get("href")
@@ -196,7 +204,7 @@ def _clean_file_url_path(part: str) -> str:
# percent-encoded: /
_reserved_chars_re = re.compile('(@|%2F)', re.IGNORECASE)
_reserved_chars_re = re.compile("(@|%2F)", re.IGNORECASE)
def _clean_url_path(path: str, is_local_path: bool) -> str:
@@ -213,12 +221,12 @@ def _clean_url_path(path: str, is_local_path: bool) -> str:
parts = _reserved_chars_re.split(path)
cleaned_parts = []
for to_clean, reserved in pairwise(itertools.chain(parts, [''])):
for to_clean, reserved in pairwise(itertools.chain(parts, [""])):
cleaned_parts.append(clean_func(to_clean))
# Normalize %xx escapes (e.g. %2f -> %2F)
cleaned_parts.append(reserved.upper())
return ''.join(cleaned_parts)
return "".join(cleaned_parts)
def _clean_link(url: str) -> str:
@@ -237,24 +245,20 @@ def _clean_link(url: str) -> str:
def _create_link_from_element(
anchor: HTMLElement,
element_attribs: Dict[str, Optional[str]],
page_url: str,
base_url: str,
) -> Optional[Link]:
"""
Convert an anchor element in a simple repository page to a Link.
Convert an anchor element's attributes in a simple repository page to a Link.
"""
href = anchor.get("href")
href = element_attribs.get("href")
if not href:
return None
url = _clean_link(urllib.parse.urljoin(base_url, href))
pyrequire = anchor.get('data-requires-python')
pyrequire = html.unescape(pyrequire) if pyrequire else None
yanked_reason = anchor.get('data-yanked')
if yanked_reason:
yanked_reason = html.unescape(yanked_reason)
pyrequire = element_attribs.get("data-requires-python")
yanked_reason = element_attribs.get("data-yanked")
link = Link(
url,
@@ -272,16 +276,20 @@ class CacheablePageContent:
self.page = page
def __eq__(self, other: object) -> bool:
return (isinstance(other, type(self)) and
self.page.url == other.page.url)
return isinstance(other, type(self)) and self.page.url == other.page.url
def __hash__(self) -> int:
return hash(self.page.url)
def with_cached_html_pages(
fn: Callable[["HTMLPage"], Iterable[Link]],
) -> Callable[["HTMLPage"], List[Link]]:
class ParseLinks(Protocol):
def __call__(
self, page: "HTMLPage", use_deprecated_html5lib: bool
) -> Iterable[Link]:
...
def with_cached_html_pages(fn: ParseLinks) -> ParseLinks:
"""
Given a function that parses an Iterable[Link] from an HTMLPage, cache the
function's result (keyed by CacheablePageContent), unless the HTMLPage
@@ -289,22 +297,25 @@ def with_cached_html_pages(
"""
@functools.lru_cache(maxsize=None)
def wrapper(cacheable_page: CacheablePageContent) -> List[Link]:
return list(fn(cacheable_page.page))
def wrapper(
cacheable_page: CacheablePageContent, use_deprecated_html5lib: bool
) -> List[Link]:
return list(fn(cacheable_page.page, use_deprecated_html5lib))
@functools.wraps(fn)
def wrapper_wrapper(page: "HTMLPage") -> List[Link]:
def wrapper_wrapper(page: "HTMLPage", use_deprecated_html5lib: bool) -> List[Link]:
if page.cache_link_parsing:
return wrapper(CacheablePageContent(page))
return list(fn(page))
return wrapper(CacheablePageContent(page), use_deprecated_html5lib)
return list(fn(page, use_deprecated_html5lib))
return wrapper_wrapper
@with_cached_html_pages
def parse_links(page: "HTMLPage") -> Iterable[Link]:
def _parse_links_html5lib(page: "HTMLPage") -> Iterable[Link]:
"""
Parse an HTML document, and yield its anchor elements as Link objects.
TODO: Remove when `html5lib` is dropped.
"""
document = html5lib.parse(
page.content,
@@ -315,6 +326,33 @@ def parse_links(page: "HTMLPage") -> Iterable[Link]:
url = page.url
base_url = _determine_base_url(document, url)
for anchor in document.findall(".//a"):
link = _create_link_from_element(
anchor.attrib,
page_url=url,
base_url=base_url,
)
if link is None:
continue
yield link
@with_cached_html_pages
def parse_links(page: "HTMLPage", use_deprecated_html5lib: bool) -> Iterable[Link]:
"""
Parse an HTML document, and yield its anchor elements as Link objects.
"""
if use_deprecated_html5lib:
yield from _parse_links_html5lib(page)
return
parser = HTMLLinkParser(page.url)
encoding = page.encoding or "utf-8"
parser.feed(page.content.decode(encoding))
url = page.url
base_url = parser.base_url or url
for anchor in parser.anchors:
link = _create_link_from_element(
anchor,
page_url=url,
@@ -351,10 +389,38 @@ class HTMLPage:
return redact_auth_from_url(self.url)
class HTMLLinkParser(HTMLParser):
"""
HTMLParser that keeps the first base HREF and a list of all anchor
elements' attributes.
"""
def __init__(self, url: str) -> None:
super().__init__(convert_charrefs=True)
self.url: str = url
self.base_url: Optional[str] = None
self.anchors: List[Dict[str, Optional[str]]] = []
def handle_starttag(self, tag: str, attrs: List[Tuple[str, Optional[str]]]) -> None:
if tag == "base" and self.base_url is None:
href = self.get_href(attrs)
if href is not None:
self.base_url = href
elif tag == "a":
self.anchors.append(dict(attrs))
def get_href(self, attrs: List[Tuple[str, Optional[str]]]) -> Optional[str]:
for name, value in attrs:
if name == "href":
return value
return None
def _handle_get_page_fail(
link: Link,
reason: Union[str, Exception],
meth: Optional[Callable[..., None]] = None
meth: Optional[Callable[..., None]] = None,
) -> None:
if meth is None:
meth = logger.debug
@@ -367,7 +433,8 @@ def _make_html_page(response: Response, cache_link_parsing: bool = True) -> HTML
response.content,
encoding=encoding,
url=response.url,
cache_link_parsing=cache_link_parsing)
cache_link_parsing=cache_link_parsing,
)
def _get_html_page(
@@ -378,37 +445,43 @@ def _get_html_page(
"_get_html_page() missing 1 required keyword argument: 'session'"
)
url = link.url.split('#', 1)[0]
url = link.url.split("#", 1)[0]
# Check for VCS schemes that do not support lookup as web pages.
vcs_scheme = _match_vcs_scheme(url)
if vcs_scheme:
logger.warning('Cannot look at %s URL %s because it does not support '
'lookup as web pages.', vcs_scheme, link)
logger.warning(
"Cannot look at %s URL %s because it does not support lookup as web pages.",
vcs_scheme,
link,
)
return None
# Tack index.html onto file:// URLs that point to directories
scheme, _, path, _, _, _ = urllib.parse.urlparse(url)
if (scheme == 'file' and os.path.isdir(urllib.request.url2pathname(path))):
if scheme == "file" and os.path.isdir(urllib.request.url2pathname(path)):
# add trailing slash if not present so urljoin doesn't trim
# final segment
if not url.endswith('/'):
url += '/'
url = urllib.parse.urljoin(url, 'index.html')
logger.debug(' file: URL is directory, getting %s', url)
if not url.endswith("/"):
url += "/"
url = urllib.parse.urljoin(url, "index.html")
logger.debug(" file: URL is directory, getting %s", url)
try:
resp = _get_html_response(url, session=session)
except _NotHTTP:
logger.warning(
'Skipping page %s because it looks like an archive, and cannot '
'be checked by a HTTP HEAD request.', link,
"Skipping page %s because it looks like an archive, and cannot "
"be checked by a HTTP HEAD request.",
link,
)
except _NotHTML as exc:
logger.warning(
'Skipping page %s because the %s request got Content-Type: %s.'
'The only supported Content-Type is text/html',
link, exc.request_desc, exc.content_type,
"Skipping page %s because the %s request got Content-Type: %s."
"The only supported Content-Type is text/html",
link,
exc.request_desc,
exc.content_type,
)
except NetworkConnectionError as exc:
_handle_get_page_fail(link, exc)
@@ -423,8 +496,7 @@ def _get_html_page(
except requests.Timeout:
_handle_get_page_fail(link, "timed out")
else:
return _make_html_page(resp,
cache_link_parsing=link.cache_link_parsing)
return _make_html_page(resp, cache_link_parsing=link.cache_link_parsing)
return None
@@ -454,7 +526,8 @@ class LinkCollector:
@classmethod
def create(
cls, session: PipSession,
cls,
session: PipSession,
options: Values,
suppress_no_index: bool = False,
index_lookup: Optional[Dict[str, List[str]]] = None,
@@ -467,8 +540,8 @@ class LinkCollector:
index_urls = [options.index_url] + options.extra_index_urls
if options.no_index and not suppress_no_index:
logger.debug(
'Ignoring indexes: %s',
','.join(redact_auth_from_url(url) for url in index_urls),
"Ignoring indexes: %s",
",".join(redact_auth_from_url(url) for url in index_urls),
)
index_urls = []
@@ -37,17 +37,14 @@ from pipenv.patched.notpip._internal.utils.logging import indent_log
from pipenv.patched.notpip._internal.utils.misc import build_netloc
from pipenv.patched.notpip._internal.utils.packaging import check_requires_python
from pipenv.patched.notpip._internal.utils.unpacking import SUPPORTED_EXTENSIONS
from pipenv.patched.notpip._internal.utils.urls import url_to_path
__all__ = ['FormatControl', 'BestCandidateResult', 'PackageFinder']
__all__ = ["FormatControl", "BestCandidateResult", "PackageFinder"]
logger = getLogger(__name__)
BuildTag = Union[Tuple[()], Tuple[int, str]]
CandidateSortingKey = (
Tuple[int, int, int, _BaseVersion, Optional[int], BuildTag]
)
CandidateSortingKey = Tuple[int, int, int, _BaseVersion, Optional[int], BuildTag]
def _check_link_requires_python(
@@ -66,27 +63,32 @@ def _check_link_requires_python(
"""
try:
is_compatible = check_requires_python(
link.requires_python, version_info=version_info,
link.requires_python,
version_info=version_info,
)
except specifiers.InvalidSpecifier:
logger.debug(
"Ignoring invalid Requires-Python (%r) for link: %s",
link.requires_python, link,
link.requires_python,
link,
)
else:
if not is_compatible:
version = '.'.join(map(str, version_info))
version = ".".join(map(str, version_info))
if not ignore_requires_python:
logger.verbose(
'Link requires a different Python (%s not in: %r): %s',
version, link.requires_python, link,
"Link requires a different Python (%s not in: %r): %s",
version,
link.requires_python,
link,
)
return False
logger.debug(
'Ignoring failed Requires-Python check (%s not in: %r) '
'for link: %s',
version, link.requires_python, link,
"Ignoring failed Requires-Python check (%s not in: %r) for link: %s",
version,
link.requires_python,
link,
)
return True
@@ -98,7 +100,7 @@ class LinkEvaluator:
Responsible for evaluating links for a particular project.
"""
_py_version_re = re.compile(r'-py([123]\.?[0-9]?)$')
_py_version_re = re.compile(r"-py([123]\.?[0-9]?)$")
# Don't include an allow_yanked default value to make sure each call
# site considers whether yanked releases are allowed. This also causes
@@ -143,9 +145,9 @@ class LinkEvaluator:
self._ignore_requires_python = ignore_requires_python
self._formats = formats
self._target_python = target_python
self._ignore_compatibility = ignore_compatibility
self.project_name = project_name
self._ignore_compatibility = ignore_compatibility
def evaluate_link(self, link: Link) -> Tuple[bool, Optional[str]]:
"""
@@ -158,8 +160,8 @@ class LinkEvaluator:
"""
version = None
if link.is_yanked and not self._allow_yanked:
reason = link.yanked_reason or '<none given>'
return (False, f'yanked for reason: {reason}')
reason = link.yanked_reason or "<none given>"
return (False, f"yanked for reason: {reason}")
if link.egg_fragment:
egg_info = link.egg_fragment
@@ -167,23 +169,21 @@ class LinkEvaluator:
else:
egg_info, ext = link.splitext()
if not ext:
return (False, 'not a file')
return (False, "not a file")
if ext not in SUPPORTED_EXTENSIONS:
return (False, f'unsupported archive format: {ext}')
return (False, f"unsupported archive format: {ext}")
if "binary" not in self._formats and ext == WHEEL_EXTENSION and not self._ignore_compatibility:
reason = 'No binaries permitted for {}'.format(
self.project_name)
reason = "No binaries permitted for {}".format(self.project_name)
return (False, reason)
if "macosx10" in link.path and ext == '.zip' and not self._ignore_compatibility:
return (False, 'macosx10 one')
return (False, "macosx10 one")
if ext == WHEEL_EXTENSION:
try:
wheel = Wheel(link.filename)
except InvalidWheelFilename:
return (False, 'invalid wheel filename')
return (False, "invalid wheel filename")
if canonicalize_name(wheel.name) != self._canonical_name:
reason = 'wrong project name (not {})'.format(
self.project_name)
reason = "wrong project name (not {})".format(self.project_name)
return (False, reason)
supported_tags = self._target_python.get_tags()
@@ -194,7 +194,7 @@ class LinkEvaluator:
reason = (
"none of the wheel's tags ({}) are compatible "
"(run pip debug --verbose to show compatible tags)".format(
', '.join(file_tags)
", ".join(file_tags)
)
)
return (False, reason)
@@ -203,26 +203,28 @@ class LinkEvaluator:
# This should be up by the self.ok_binary check, but see issue 2700.
if "source" not in self._formats and ext != WHEEL_EXTENSION:
reason = f'No sources permitted for {self.project_name}'
reason = f"No sources permitted for {self.project_name}"
return (False, reason)
if not version:
version = _extract_version_from_fragment(
egg_info, self._canonical_name,
egg_info,
self._canonical_name,
)
if not version:
reason = f'Missing project version for {self.project_name}'
reason = f"Missing project version for {self.project_name}"
return (False, reason)
match = self._py_version_re.search(version)
if match:
version = version[:match.start()]
version = version[: match.start()]
py_version = match.group(1)
if py_version != self._target_python.py_version:
return (False, 'Python version is incorrect')
return (False, "Python version is incorrect")
supports_python = _check_link_requires_python(
link, version_info=self._target_python.py_version_info,
link,
version_info=self._target_python.py_version_info,
ignore_requires_python=self._ignore_requires_python,
)
if not supports_python and not self._ignore_compatibility:
@@ -230,7 +232,7 @@ class LinkEvaluator:
# _log_skipped_link().
return (False, None)
logger.debug('Found link %s, version: %s', link, version)
logger.debug("Found link %s, version: %s", link, version)
return (True, version)
@@ -257,8 +259,8 @@ def filter_unallowed_hashes(
"""
if not hashes:
logger.debug(
'Given no hashes to check %s links for project %r: '
'discarding no candidates',
"Given no hashes to check %s links for project %r: "
"discarding no candidates",
len(candidates),
project_name,
)
@@ -288,22 +290,22 @@ def filter_unallowed_hashes(
filtered = list(candidates)
if len(filtered) == len(candidates):
discard_message = 'discarding no candidates'
discard_message = "discarding no candidates"
else:
discard_message = 'discarding {} non-matches:\n {}'.format(
discard_message = "discarding {} non-matches:\n {}".format(
len(non_matches),
'\n '.join(str(candidate.link) for candidate in non_matches)
"\n ".join(str(candidate.link) for candidate in non_matches),
)
logger.debug(
'Checked %s links for project %r against %s hashes '
'(%s matches, %s no digest): %s',
"Checked %s links for project %r against %s hashes "
"(%s matches, %s no digest): %s",
len(candidates),
project_name,
hashes.digest_count,
match_count,
len(matches_or_no_digest) - match_count,
discard_message
discard_message,
)
return filtered
@@ -360,13 +362,11 @@ class BestCandidateResult:
self.best_candidate = best_candidate
def iter_all(self) -> Iterable[InstallationCandidate]:
"""Iterate through all candidates.
"""
"""Iterate through all candidates."""
return iter(self._candidates)
def iter_applicable(self) -> Iterable[InstallationCandidate]:
"""Iterate through the applicable candidates.
"""
"""Iterate through the applicable candidates."""
return iter(self._applicable_candidates)
@@ -450,7 +450,8 @@ class CandidateEvaluator:
allow_prereleases = self._allow_all_prereleases or None
specifier = self._specifier
versions = {
str(v) for v in specifier.filter(
str(v)
for v in specifier.filter(
# We turn the version object into a str here because otherwise
# when we're debundled but setuptools isn't, Python will see
# packaging.version.Version and
@@ -464,9 +465,7 @@ class CandidateEvaluator:
}
# Again, converting version to str to deal with debundling.
applicable_candidates = [
c for c in candidates if str(c.version) in versions
]
applicable_candidates = [c for c in candidates if str(c.version) in versions]
filtered_applicable_candidates = filter_unallowed_hashes(
candidates=applicable_candidates,
@@ -518,9 +517,11 @@ class CandidateEvaluator:
# can raise InvalidWheelFilename
wheel = Wheel(link.filename)
try:
pri = -(wheel.find_most_preferred_tag(
valid_tags, self._wheel_tag_preferences
))
pri = -(
wheel.find_most_preferred_tag(
valid_tags, self._wheel_tag_preferences
)
)
except ValueError:
if not ignore_compatibility:
raise UnsupportedWheel(
@@ -528,10 +529,11 @@ class CandidateEvaluator:
"can't be sorted.".format(wheel.filename)
)
pri = -(support_num)
if self._prefer_binary:
binary_preference = 1
if wheel.build_tag is not None:
match = re.match(r'^(\d+)(.*)$', wheel.build_tag)
match = re.match(r"^(\d+)(.*)$", wheel.build_tag)
build_tag_groups = match.groups()
build_tag = (int(build_tag_groups[0]), build_tag_groups[1])
else: # sdist
@@ -539,8 +541,12 @@ class CandidateEvaluator:
has_allowed_hash = int(link.is_hash_allowed(self._hashes))
yank_value = -1 * int(link.is_yanked) # -1 for yanked.
return (
has_allowed_hash, yank_value, binary_preference, candidate.version,
pri, build_tag,
has_allowed_hash,
yank_value,
binary_preference,
candidate.version,
pri,
build_tag,
)
def sort_best_candidate(
@@ -586,6 +592,7 @@ class PackageFinder:
link_collector: LinkCollector,
target_python: TargetPython,
allow_yanked: bool,
use_deprecated_html5lib: bool,
format_control: Optional[FormatControl] = None,
candidate_prefs: Optional[CandidatePreferences] = None,
ignore_requires_python: Optional[bool] = None,
@@ -612,6 +619,7 @@ class PackageFinder:
self._link_collector = link_collector
self._target_python = target_python
self._ignore_compatibility = ignore_compatibility
self._use_deprecated_html5lib = use_deprecated_html5lib
self.format_control = format_control
@@ -628,6 +636,8 @@ class PackageFinder:
link_collector: LinkCollector,
selection_prefs: SelectionPreferences,
target_python: Optional[TargetPython] = None,
*,
use_deprecated_html5lib: bool,
) -> "PackageFinder":
"""Create a PackageFinder.
@@ -652,6 +662,7 @@ class PackageFinder:
allow_yanked=selection_prefs.allow_yanked,
format_control=selection_prefs.format_control,
ignore_requires_python=selection_prefs.ignore_requires_python,
use_deprecated_html5lib=use_deprecated_html5lib,
)
@property
@@ -727,7 +738,7 @@ class PackageFinder:
if link not in self._logged_links:
# Put the link at the end so the reason is more visible and because
# the link string is usually very long.
logger.debug('Skipping link: %s: %s', reason, link)
logger.debug("Skipping link: %s: %s", reason, link)
self._logged_links.add(link)
def get_install_candidate(
@@ -767,13 +778,14 @@ class PackageFinder:
self, project_url: Link, link_evaluator: LinkEvaluator
) -> List[InstallationCandidate]:
logger.debug(
'Fetching project page and analyzing links: %s', project_url,
"Fetching project page and analyzing links: %s",
project_url,
)
html_page = self._link_collector.fetch_page(project_url)
if html_page is None:
return []
page_links = list(parse_links(html_page))
page_links = list(parse_links(html_page, self._use_deprecated_html5lib))
with indent_log():
package_links = self.evaluate_links(
@@ -823,7 +835,14 @@ class PackageFinder:
)
if logger.isEnabledFor(logging.DEBUG) and file_candidates:
paths = [url_to_path(c.link.url) for c in file_candidates]
paths = []
for candidate in file_candidates:
assert candidate.link.url # we need to have a URL
try:
paths.append(candidate.link.file_path)
except Exception:
paths.append(candidate.link.url) # it's not a local file
logger.debug("Local files found: %s", ", ".join(paths))
# This is an intentional priority ordering
@@ -835,8 +854,7 @@ class PackageFinder:
specifier: Optional[specifiers.BaseSpecifier] = None,
hashes: Optional[Hashes] = None,
) -> CandidateEvaluator:
"""Create a CandidateEvaluator object to use.
"""
"""Create a CandidateEvaluator object to use."""
candidate_prefs = self._candidate_prefs
return CandidateEvaluator.create(
project_name=project_name,
@@ -881,54 +899,60 @@ class PackageFinder:
"""
hashes = req.hashes(trust_internet=False)
best_candidate_result = self.find_best_candidate(
req.name, specifier=req.specifier, hashes=hashes,
req.name,
specifier=req.specifier,
hashes=hashes,
)
best_candidate = best_candidate_result.best_candidate
installed_version: Optional[_BaseVersion] = None
if req.satisfied_by is not None:
installed_version = parse_version(req.satisfied_by.version)
installed_version = req.satisfied_by.version
def _format_versions(cand_iter: Iterable[InstallationCandidate]) -> str:
# This repeated parse_version and str() conversion is needed to
# handle different vendoring sources from pip and pkg_resources.
# If we stop using the pkg_resources provided specifier and start
# using our own, we can drop the cast to str().
return ", ".join(sorted(
{str(c.version) for c in cand_iter},
key=parse_version,
)) or "none"
return (
", ".join(
sorted(
{str(c.version) for c in cand_iter},
key=parse_version,
)
)
or "none"
)
if installed_version is None and best_candidate is None:
logger.critical(
'Could not find a version that satisfies the requirement %s '
'(from versions: %s)',
"Could not find a version that satisfies the requirement %s "
"(from versions: %s)",
req,
_format_versions(best_candidate_result.iter_all()),
)
raise DistributionNotFound(
'No matching distribution found for {}'.format(
req)
"No matching distribution found for {}".format(req)
)
best_installed = False
if installed_version and (
best_candidate is None or
best_candidate.version <= installed_version):
best_candidate is None or best_candidate.version <= installed_version
):
best_installed = True
if not upgrade and installed_version is not None:
if best_installed:
logger.debug(
'Existing installed version (%s) is most up-to-date and '
'satisfies requirement',
"Existing installed version (%s) is most up-to-date and "
"satisfies requirement",
installed_version,
)
else:
logger.debug(
'Existing installed version (%s) satisfies requirement '
'(most up-to-date version is %s)',
"Existing installed version (%s) satisfies requirement "
"(most up-to-date version is %s)",
installed_version,
best_candidate.version,
)
@@ -937,15 +961,14 @@ class PackageFinder:
if best_installed:
# We have an existing version, and its the best version
logger.debug(
'Installed version (%s) is most up-to-date (past versions: '
'%s)',
"Installed version (%s) is most up-to-date (past versions: %s)",
installed_version,
_format_versions(best_candidate_result.iter_applicable()),
)
raise BestVersionAlreadyInstalled
logger.debug(
'Using version %s (newest of versions: %s)',
"Using version %s (newest of versions: %s)",
best_candidate.version,
_format_versions(best_candidate_result.iter_applicable()),
)
@@ -38,17 +38,38 @@ __all__ = [
logger = logging.getLogger(__name__)
if os.environ.get("_PIP_LOCATIONS_NO_WARN_ON_MISMATCH"):
_MISMATCH_LEVEL = logging.DEBUG
else:
_MISMATCH_LEVEL = logging.WARNING
_PLATLIBDIR: str = getattr(sys, "platlibdir", "lib")
_USE_SYSCONFIG_DEFAULT = sys.version_info >= (3, 10)
def _should_use_sysconfig() -> bool:
"""This function determines the value of _USE_SYSCONFIG.
By default, pip uses sysconfig on Python 3.10+.
But Python distributors can override this decision by setting:
sysconfig._PIP_USE_SYSCONFIG = True / False
Rationale in https://github.com/pypa/pip/issues/10647
This is a function for testability, but should be constant during any one
run.
"""
return bool(getattr(sysconfig, "_PIP_USE_SYSCONFIG", _USE_SYSCONFIG_DEFAULT))
_USE_SYSCONFIG = _should_use_sysconfig()
# Be noisy about incompatibilities if this platforms "should" be using
# sysconfig, but is explicitly opting out and using distutils instead.
if _USE_SYSCONFIG_DEFAULT and not _USE_SYSCONFIG:
_MISMATCH_LEVEL = logging.WARNING
else:
_MISMATCH_LEVEL = logging.DEBUG
def _looks_like_bpo_44860() -> bool:
"""The resolution to bpo-44860 will change this incorrect platlib.
See <https://bugs.python.org/issue44860>.
"""
from distutils.command.install import INSTALL_SCHEMES # type: ignore
@@ -62,6 +83,8 @@ def _looks_like_bpo_44860() -> bool:
def _looks_like_red_hat_patched_platlib_purelib(scheme: Dict[str, str]) -> bool:
platlib = scheme["platlib"]
if "/$platlibdir/" in platlib:
platlib = platlib.replace("/$platlibdir/", f"/{_PLATLIBDIR}/")
if "/lib64/" not in platlib:
return False
unpatched = platlib.replace("/lib64/", "/lib/")
@@ -111,14 +134,27 @@ def _looks_like_red_hat_scheme() -> bool:
)
@functools.lru_cache(maxsize=None)
def _looks_like_slackware_scheme() -> bool:
"""Slackware patches sysconfig but fails to patch distutils and site.
Slackware changes sysconfig's user scheme to use ``"lib64"`` for the lib
path, but does not do the same to the site module.
"""
if user_site is None: # User-site not available.
return False
try:
paths = sysconfig.get_paths(scheme="posix_user", expand=False)
except KeyError: # User-site not available.
return False
return "/lib64/" in paths["purelib"] and "/lib64/" not in user_site
@functools.lru_cache(maxsize=None)
def _looks_like_msys2_mingw_scheme() -> bool:
"""MSYS2 patches distutils and sysconfig to use a UNIX-like scheme.
However, MSYS2 incorrectly patches sysconfig ``nt`` scheme. The fix is
likely going to be included in their 3.10 release, so we ignore the warning.
See msys2/MINGW-packages#9319.
MSYS2 MINGW's patch uses lowercase ``"lib"`` instead of the usual uppercase,
and is missing the final ``"site-packages"``.
"""
@@ -190,7 +226,7 @@ def get_scheme(
isolated: bool = False,
prefix: Optional[str] = None,
) -> Scheme:
old = _distutils.get_scheme(
new = _sysconfig.get_scheme(
dist_name,
user=user,
home=home,
@@ -198,7 +234,10 @@ def get_scheme(
isolated=isolated,
prefix=prefix,
)
new = _sysconfig.get_scheme(
if _USE_SYSCONFIG:
return new
old = _distutils.get_scheme(
dist_name,
user=user,
home=home,
@@ -263,6 +302,17 @@ def get_scheme(
if skip_bpo_44860:
continue
# Slackware incorrectly patches posix_user to use lib64 instead of lib,
# but not usersite to match the location.
skip_slackware_user_scheme = (
user
and k in ("platlib", "purelib")
and not WINDOWS
and _looks_like_slackware_scheme()
)
if skip_slackware_user_scheme:
continue
# Both Debian and Red Hat patch Python to place the system site under
# /usr/local instead of /usr. Debian also places lib in dist-packages
# instead of site-packages, but the /usr/local check should cover it.
@@ -296,6 +346,18 @@ def get_scheme(
if skip_msys2_mingw_bug:
continue
# CPython's POSIX install script invokes pip (via ensurepip) against the
# interpreter located in the source tree, not the install site. This
# triggers special logic in sysconfig that's not present in distutils.
# https://github.com/python/cpython/blob/8c21941ddaf/Lib/sysconfig.py#L178-L194
skip_cpython_build = (
sysconfig.is_python_build(check_home=True)
and not WINDOWS
and k in ("headers", "include", "platinclude")
)
if skip_cpython_build:
continue
warning_contexts.append((old_v, new_v, f"scheme.{k}"))
if not warning_contexts:
@@ -315,10 +377,12 @@ def get_scheme(
)
if any(default_old[k] != getattr(old, k) for k in SCHEME_KEYS):
deprecated(
"Configuring installation scheme with distutils config files "
"is deprecated and will no longer work in the near future. If you "
"are using a Homebrew or Linuxbrew Python, please see discussion "
"at https://github.com/Homebrew/homebrew-core/issues/76621",
reason=(
"Configuring installation scheme with distutils config files "
"is deprecated and will no longer work in the near future. If you "
"are using a Homebrew or Linuxbrew Python, please see discussion "
"at https://github.com/Homebrew/homebrew-core/issues/76621"
),
replacement=None,
gone_in=None,
)
@@ -333,8 +397,11 @@ def get_scheme(
def get_bin_prefix() -> str:
old = _distutils.get_bin_prefix()
new = _sysconfig.get_bin_prefix()
if _USE_SYSCONFIG:
return new
old = _distutils.get_bin_prefix()
if _warn_if_mismatch(pathlib.Path(old), pathlib.Path(new), key="bin_prefix"):
_log_context()
return old
@@ -363,8 +430,11 @@ def _looks_like_deb_system_dist_packages(value: str) -> bool:
def get_purelib() -> str:
"""Return the default pure-Python lib location."""
old = _distutils.get_purelib()
new = _sysconfig.get_purelib()
if _USE_SYSCONFIG:
return new
old = _distutils.get_purelib()
if _looks_like_deb_system_dist_packages(old):
return old
if _warn_if_mismatch(pathlib.Path(old), pathlib.Path(new), key="purelib"):
@@ -374,8 +444,11 @@ def get_purelib() -> str:
def get_platlib() -> str:
"""Return the default platform-shared lib location."""
old = _distutils.get_platlib()
new = _sysconfig.get_platlib()
if _USE_SYSCONFIG:
return new
old = _distutils.get_platlib()
if _looks_like_deb_system_dist_packages(old):
return old
if _warn_if_mismatch(pathlib.Path(old), pathlib.Path(new), key="platlib"):
@@ -383,10 +456,47 @@ def get_platlib() -> str:
return old
def _deduplicated(v1: str, v2: str) -> List[str]:
"""Deduplicate values from a list."""
if v1 == v2:
return [v1]
return [v1, v2]
def _looks_like_apple_library(path: str) -> bool:
"""Apple patches sysconfig to *always* look under */Library/Python*."""
if sys.platform[:6] != "darwin":
return False
return path == f"/Library/Python/{get_major_minor_version()}/site-packages"
def get_prefixed_libs(prefix: str) -> List[str]:
"""Return the lib locations under ``prefix``."""
old_pure, old_plat = _distutils.get_prefixed_libs(prefix)
new_pure, new_plat = _sysconfig.get_prefixed_libs(prefix)
if _USE_SYSCONFIG:
return _deduplicated(new_pure, new_plat)
old_pure, old_plat = _distutils.get_prefixed_libs(prefix)
old_lib_paths = _deduplicated(old_pure, old_plat)
# Apple's Python (shipped with Xcode and Command Line Tools) hard-code
# platlib and purelib to '/Library/Python/X.Y/site-packages'. This will
# cause serious build isolation bugs when Apple starts shipping 3.10 because
# pip will install build backends to the wrong location. This tells users
# who is at fault so Apple may notice it and fix the issue in time.
if all(_looks_like_apple_library(p) for p in old_lib_paths):
deprecated(
reason=(
"Python distributed by Apple's Command Line Tools incorrectly "
"patches sysconfig to always point to '/Library/Python'. This "
"will cause build isolation to operate incorrectly on Python "
"3.10 or later. Please help report this to Apple so they can "
"fix this. https://developer.apple.com/bug-reporting/"
),
replacement=None,
gone_in=None,
)
return old_lib_paths
warned = [
_warn_if_mismatch(
@@ -403,6 +513,4 @@ def get_prefixed_libs(prefix: str) -> List[str]:
if any(warned):
_log_context(prefix=prefix)
if old_pure == old_plat:
return [old_pure]
return [old_pure, old_plat]
return old_lib_paths
+1 -2
View File
@@ -1,8 +1,7 @@
from typing import List, Optional
def main(args=None):
# type: (Optional[List[str]]) -> int
def main(args: Optional[List[str]] = None) -> int:
"""This is preserved for old console scripts that may still be referencing
it.
@@ -1,10 +1,13 @@
from typing import List, Optional
from .base import BaseDistribution, BaseEnvironment
from .base import BaseDistribution, BaseEnvironment, FilesystemWheel, MemoryWheel, Wheel
__all__ = [
"BaseDistribution",
"BaseEnvironment",
"FilesystemWheel",
"MemoryWheel",
"Wheel",
"get_default_environment",
"get_environment",
"get_wheel_distribution",
@@ -35,7 +38,18 @@ def get_environment(paths: Optional[List[str]]) -> BaseEnvironment:
return Environment.from_paths(paths)
def get_wheel_distribution(wheel_path: str, canonical_name: str) -> BaseDistribution:
def get_directory_distribution(directory: str) -> BaseDistribution:
"""Get the distribution metadata representation in the specified directory.
This returns a Distribution instance from the chosen backend based on
the given on-disk ``.dist-info`` directory.
"""
from .pkg_resources import Distribution
return Distribution.from_directory(directory)
def get_wheel_distribution(wheel: Wheel, canonical_name: str) -> BaseDistribution:
"""Get the representation of the specified wheel's distribution metadata.
This returns a Distribution instance from the chosen backend based on
@@ -45,4 +59,4 @@ def get_wheel_distribution(wheel_path: str, canonical_name: str) -> BaseDistribu
"""
from .pkg_resources import Distribution
return Distribution.from_wheel(wheel_path, canonical_name)
return Distribution.from_wheel(wheel, canonical_name)
+321 -17
View File
@@ -1,8 +1,12 @@
import csv
import email.message
import json
import logging
import pathlib
import re
import zipfile
from typing import (
IO,
TYPE_CHECKING,
Collection,
Container,
@@ -10,28 +14,39 @@ from typing import (
Iterator,
List,
Optional,
Tuple,
Union,
)
from pipenv.patched.notpip._vendor.packaging.requirements import Requirement
from pipenv.patched.notpip._vendor.packaging.specifiers import InvalidSpecifier, SpecifierSet
from pipenv.patched.notpip._vendor.packaging.utils import NormalizedName
from pipenv.patched.notpip._vendor.packaging.version import LegacyVersion, Version
from pipenv.patched.notpip._internal.exceptions import NoneMetadataError
from pipenv.patched.notpip._internal.locations import site_packages, user_site
from pipenv.patched.notpip._internal.models.direct_url import (
DIRECT_URL_METADATA_NAME,
DirectUrl,
DirectUrlValidationError,
)
from pipenv.patched.notpip._internal.utils.misc import stdlib_pkgs # TODO: Move definition here.
from pipenv.patched.notpip._internal.utils.compat import stdlib_pkgs # TODO: Move definition here.
from pipenv.patched.notpip._internal.utils.egg_link import (
egg_link_path_from_location,
egg_link_path_from_sys_path,
)
from pipenv.patched.notpip._internal.utils.misc import is_local, normalize_path
from pipenv.patched.notpip._internal.utils.urls import url_to_path
if TYPE_CHECKING:
from typing import Protocol
from pipenv.patched.notpip._vendor.packaging.utils import NormalizedName
else:
Protocol = object
DistributionVersion = Union[LegacyVersion, Version]
InfoPath = Union[str, pathlib.PurePosixPath]
logger = logging.getLogger(__name__)
@@ -49,7 +64,43 @@ class BaseEntryPoint(Protocol):
raise NotImplementedError()
def _convert_installed_files_path(
entry: Tuple[str, ...],
info: Tuple[str, ...],
) -> str:
"""Convert a legacy installed-files.txt path into modern RECORD path.
The legacy format stores paths relative to the info directory, while the
modern format stores paths relative to the package root, e.g. the
site-packages directory.
:param entry: Path parts of the installed-files.txt entry.
:param info: Path parts of the egg-info directory relative to package root.
:returns: The converted entry.
For best compatibility with symlinks, this does not use ``abspath()`` or
``Path.resolve()``, but tries to work with path parts:
1. While ``entry`` starts with ``..``, remove the equal amounts of parts
from ``info``; if ``info`` is empty, start appending ``..`` instead.
2. Join the two directly.
"""
while entry and entry[0] == "..":
if not info or info[-1] == "..":
info += ("..",)
else:
info = info[:-1]
entry = entry[1:]
return str(pathlib.Path(*info, *entry))
class BaseDistribution(Protocol):
def __repr__(self) -> str:
return f"{self.raw_name} {self.version} ({self.location})"
def __str__(self) -> str:
return f"{self.raw_name} {self.version}"
@property
def location(self) -> Optional[str]:
"""Where the distribution is loaded from.
@@ -65,8 +116,50 @@ class BaseDistribution(Protocol):
raise NotImplementedError()
@property
def info_directory(self) -> Optional[str]:
"""Location of the .[egg|dist]-info directory.
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 installed_location(self) -> Optional[str]:
"""The distribution's "installed" location.
This should generally be a ``site-packages`` directory. This is
usually ``dist.location``, except for legacy develop-installed packages,
where ``dist.location`` is the source code location, and this is where
the ``.egg-link`` file is.
The returned location is normalized (in particular, with symlinks removed).
"""
egg_link = egg_link_path_from_location(self.raw_name)
if egg_link:
location = egg_link
elif self.location:
location = self.location
else:
return None
return normalize_path(location)
@property
def info_location(self) -> Optional[str]:
"""Location of the .[egg|dist]-info directory or file.
Similarly to ``location``, a string value is not necessarily a
filesystem path. ``None`` means the distribution is created in-memory.
@@ -81,13 +174,80 @@ class BaseDistribution(Protocol):
raise NotImplementedError()
@property
def canonical_name(self) -> "NormalizedName":
def installed_by_distutils(self) -> bool:
"""Whether this distribution is installed with legacy distutils format.
A distribution installed with "raw" distutils not patched by setuptools
uses one single file at ``info_location`` to store metadata. We need to
treat this specially on uninstallation.
"""
info_location = self.info_location
if not info_location:
return False
return pathlib.Path(info_location).is_file()
@property
def installed_as_egg(self) -> bool:
"""Whether this distribution is installed as an egg.
This usually indicates the distribution was installed by (older versions
of) easy_install.
"""
location = self.location
if not location:
return False
return location.endswith(".egg")
@property
def installed_with_setuptools_egg_info(self) -> bool:
"""Whether this distribution is installed with the ``.egg-info`` format.
This usually indicates the distribution was installed with setuptools
with an old pip version or with ``single-version-externally-managed``.
Note that this ensure the metadata store is a directory. distutils can
also installs an ``.egg-info``, but as a file, not a directory. This
property is *False* for that case. Also see ``installed_by_distutils``.
"""
info_location = self.info_location
if not info_location:
return False
if not info_location.endswith(".egg-info"):
return False
return pathlib.Path(info_location).is_dir()
@property
def installed_with_dist_info(self) -> bool:
"""Whether this distribution is installed with the "modern format".
This indicates a "modern" installation, e.g. storing metadata in the
``.dist-info`` directory. This applies to installations made by
setuptools (but through pip, not directly), or anything using the
standardized build backend interface (PEP 517).
"""
info_location = self.info_location
if not info_location:
return False
if not info_location.endswith(".dist-info"):
return False
return pathlib.Path(info_location).is_dir()
@property
def canonical_name(self) -> NormalizedName:
raise NotImplementedError()
@property
def version(self) -> DistributionVersion:
raise NotImplementedError()
@property
def setuptools_filename(self) -> str:
"""Convert a project name to its setuptools-compatible filename.
This is a copy of ``pkg_resources.to_filename()`` for compatibility.
"""
return self.raw_name.replace("-", "_")
@property
def direct_url(self) -> Optional[DirectUrl]:
"""Obtain a DirectUrl from this distribution.
@@ -116,29 +276,62 @@ class BaseDistribution(Protocol):
@property
def installer(self) -> str:
raise NotImplementedError()
try:
installer_text = self.read_text("INSTALLER")
except (OSError, ValueError, NoneMetadataError):
return "" # Fail silently if the installer file cannot be read.
for line in installer_text.splitlines():
cleaned_line = line.strip()
if cleaned_line:
return cleaned_line
return ""
@property
def editable(self) -> bool:
raise NotImplementedError()
return bool(self.editable_project_location)
@property
def local(self) -> bool:
raise NotImplementedError()
"""If distribution is installed in the current virtual environment.
Always True if we're not in a virtualenv.
"""
if self.installed_location is None:
return False
return is_local(self.installed_location)
@property
def in_usersite(self) -> bool:
raise NotImplementedError()
if self.installed_location is None or user_site is None:
return False
return self.installed_location.startswith(normalize_path(user_site))
@property
def in_site_packages(self) -> bool:
if self.installed_location is None or site_packages is None:
return False
return self.installed_location.startswith(normalize_path(site_packages))
def is_file(self, path: InfoPath) -> bool:
"""Check whether an entry in the info directory is a file."""
raise NotImplementedError()
def read_text(self, name: str) -> str:
"""Read a file in the .dist-info (or .egg-info) directory.
def iterdir(self, path: InfoPath) -> Iterator[pathlib.PurePosixPath]:
"""Iterate through a directory in the info directory.
Should raise ``FileNotFoundError`` if ``name`` does not exist in the
metadata directory.
Each item yielded would be a path relative to the info directory.
:raise FileNotFoundError: If ``name`` does not exist in the directory.
:raise NotADirectoryError: If ``name`` does not point to a directory.
"""
raise NotImplementedError()
def read_text(self, path: InfoPath) -> str:
"""Read a file in the info directory.
:raise FileNotFoundError: If ``name`` does not exist in the directory.
:raise NoneMetadataError: If ``name`` exists in the info directory, but
cannot be read.
"""
raise NotImplementedError()
@@ -147,7 +340,13 @@ class BaseDistribution(Protocol):
@property
def metadata(self) -> email.message.Message:
"""Metadata of distribution parsed from e.g. METADATA or PKG-INFO."""
"""Metadata of distribution parsed from e.g. METADATA or PKG-INFO.
This should return an empty message if the metadata file is unavailable.
:raises NoneMetadataError: If the metadata file is available, but does
not contain valid metadata.
"""
raise NotImplementedError()
@property
@@ -159,12 +358,89 @@ class BaseDistribution(Protocol):
def raw_name(self) -> str:
"""Value of "Name:" in distribution metadata."""
# The metadata should NEVER be missing the Name: key, but if it somehow
# does not, fall back to the known canonical name.
# does, fall back to the known canonical name.
return self.metadata.get("Name", self.canonical_name)
@property
def requires_python(self) -> SpecifierSet:
"""Value of "Requires-Python:" in distribution metadata.
If the key does not exist or contains an invalid value, an empty
SpecifierSet should be returned.
"""
value = self.metadata.get("Requires-Python")
if value is None:
return SpecifierSet()
try:
# Convert to str to satisfy the type checker; this can be a Header object.
spec = SpecifierSet(str(value))
except InvalidSpecifier as e:
message = "Package %r has an invalid Requires-Python: %s"
logger.warning(message, self.raw_name, e)
return SpecifierSet()
return spec
def iter_dependencies(self, extras: Collection[str] = ()) -> Iterable[Requirement]:
"""Dependencies of this distribution.
For modern .dist-info distributions, this is the collection of
"Requires-Dist:" entries in distribution metadata.
"""
raise NotImplementedError()
def iter_provided_extras(self) -> Iterable[str]:
"""Extras provided by this distribution.
For modern .dist-info distributions, this is the collection of
"Provides-Extra:" entries in distribution metadata.
"""
raise NotImplementedError()
def _iter_declared_entries_from_record(self) -> Optional[Iterator[str]]:
try:
text = self.read_text("RECORD")
except FileNotFoundError:
return None
# This extra Path-str cast normalizes entries.
return (str(pathlib.Path(row[0])) for row in csv.reader(text.splitlines()))
def _iter_declared_entries_from_legacy(self) -> Optional[Iterator[str]]:
try:
text = self.read_text("installed-files.txt")
except FileNotFoundError:
return None
paths = (p for p in text.splitlines(keepends=False) if p)
root = self.location
info = self.info_location
if root is None or info is None:
return paths
try:
info_rel = pathlib.Path(info).relative_to(root)
except ValueError: # info is not relative to root.
return paths
if not info_rel.parts: # info *is* root.
return paths
return (
_convert_installed_files_path(pathlib.Path(p).parts, info_rel.parts)
for p in paths
)
def iter_declared_entries(self) -> Optional[Iterator[str]]:
"""Iterate through file entires declared in this distribution.
For modern .dist-info distributions, this is the files listed in the
``RECORD`` metadata file. For legacy setuptools distributions, this
comes from ``installed-files.txt``, with entries normalized to be
compatible with the format used by ``RECORD``.
:return: An iterator for listed entries, or None if the distribution
contains neither ``RECORD`` nor ``installed-files.txt``.
"""
return (
self._iter_declared_entries_from_record()
or self._iter_declared_entries_from_legacy()
)
class BaseEnvironment:
"""An environment containing distributions to introspect."""
@@ -178,7 +454,11 @@ class BaseEnvironment:
raise NotImplementedError()
def get_distribution(self, name: str) -> Optional["BaseDistribution"]:
"""Given a requirement name, return the installed distributions."""
"""Given a requirement name, return the installed distributions.
The name may not be normalized. The implementation must canonicalize
it for lookup.
"""
raise NotImplementedError()
def _iter_distributions(self) -> Iterator["BaseDistribution"]:
@@ -240,3 +520,27 @@ class BaseEnvironment:
if user_only:
it = (d for d in it if d.in_usersite)
return (d for d in it if d.canonical_name not in skip)
class Wheel(Protocol):
location: str
def as_zipfile(self) -> zipfile.ZipFile:
raise NotImplementedError()
class FilesystemWheel(Wheel):
def __init__(self, location: str) -> None:
self.location = location
def as_zipfile(self) -> zipfile.ZipFile:
return zipfile.ZipFile(self.location, allowZip64=True)
class MemoryWheel(Wheel):
def __init__(self, location: str, stream: IO[bytes]) -> None:
self.location = location
self.stream = stream
def as_zipfile(self) -> zipfile.ZipFile:
return zipfile.ZipFile(self.stream, allowZip64=True)
@@ -1,29 +1,28 @@
import email.message
import email.parser
import logging
import os
import pathlib
import zipfile
from typing import (
TYPE_CHECKING,
Collection,
Iterable,
Iterator,
List,
NamedTuple,
Optional,
)
from typing import Collection, Iterable, Iterator, List, Mapping, NamedTuple, Optional
from pipenv.patched.notpip._vendor import pkg_resources
from pipenv.patched.notpip._vendor.packaging.requirements import Requirement
from pipenv.patched.notpip._vendor.packaging.utils import canonicalize_name
from pipenv.patched.notpip._vendor.packaging.utils import NormalizedName, canonicalize_name
from pipenv.patched.notpip._vendor.packaging.version import parse as parse_version
from pipenv.patched.notpip._internal.utils import misc # TODO: Move definition here.
from pipenv.patched.notpip._internal.utils.packaging import get_installer, get_metadata
from pipenv.patched.notpip._internal.utils.wheel import pkg_resources_distribution_for_wheel
from pipenv.patched.notpip._internal.exceptions import InvalidWheel, NoneMetadataError, UnsupportedWheel
from pipenv.patched.notpip._internal.utils.misc import display_path
from pipenv.patched.notpip._internal.utils.wheel import parse_wheel, read_wheel_metadata_file
from .base import BaseDistribution, BaseEntryPoint, BaseEnvironment, DistributionVersion
if TYPE_CHECKING:
from pipenv.patched.notpip._vendor.packaging.utils import NormalizedName
from .base import (
BaseDistribution,
BaseEntryPoint,
BaseEnvironment,
DistributionVersion,
InfoPath,
Wheel,
)
logger = logging.getLogger(__name__)
@@ -34,14 +33,91 @@ class EntryPoint(NamedTuple):
group: str
class WheelMetadata:
"""IMetadataProvider that reads metadata files from a dictionary.
This also maps metadata decoding exceptions to our internal exception type.
"""
def __init__(self, metadata: Mapping[str, bytes], wheel_name: str) -> None:
self._metadata = metadata
self._wheel_name = wheel_name
def has_metadata(self, name: str) -> bool:
return name in self._metadata
def get_metadata(self, name: str) -> str:
try:
return self._metadata[name].decode()
except UnicodeDecodeError as e:
# Augment the default error with the origin of the file.
raise UnsupportedWheel(
f"Error decoding metadata for {self._wheel_name}: {e} in {name} file"
)
def get_metadata_lines(self, name: str) -> Iterable[str]:
return pkg_resources.yield_lines(self.get_metadata(name))
def metadata_isdir(self, name: str) -> bool:
return False
def metadata_listdir(self, name: str) -> List[str]:
return []
def run_script(self, script_name: str, namespace: str) -> None:
pass
class Distribution(BaseDistribution):
def __init__(self, dist: pkg_resources.Distribution) -> None:
self._dist = dist
@classmethod
def from_wheel(cls, path: str, name: str) -> "Distribution":
with zipfile.ZipFile(path, allowZip64=True) as zf:
dist = pkg_resources_distribution_for_wheel(zf, name, path)
def from_directory(cls, directory: str) -> "Distribution":
dist_dir = directory.rstrip(os.sep)
# Build a PathMetadata object, from path to metadata. :wink:
base_dir, dist_dir_name = os.path.split(dist_dir)
metadata = pkg_resources.PathMetadata(base_dir, dist_dir)
# Determine the correct Distribution object type.
if dist_dir.endswith(".egg-info"):
dist_cls = pkg_resources.Distribution
dist_name = os.path.splitext(dist_dir_name)[0]
else:
assert dist_dir.endswith(".dist-info")
dist_cls = pkg_resources.DistInfoDistribution
dist_name = os.path.splitext(dist_dir_name)[0].split("-")[0]
dist = dist_cls(base_dir, project_name=dist_name, metadata=metadata)
return cls(dist)
@classmethod
def from_wheel(cls, wheel: Wheel, name: str) -> "Distribution":
"""Load the distribution from a given wheel.
:raises InvalidWheel: Whenever loading of the wheel causes a
:py:exc:`zipfile.BadZipFile` exception to be thrown.
:raises UnsupportedWheel: If the wheel is a valid zip, but malformed
internally.
"""
try:
with wheel.as_zipfile() as zf:
info_dir, _ = parse_wheel(zf, name)
metadata_text = {
path.split("/", 1)[-1]: read_wheel_metadata_file(zf, path)
for path in zf.namelist()
if path.startswith(f"{info_dir}/")
}
except zipfile.BadZipFile as e:
raise InvalidWheel(wheel.location, name) from e
except UnsupportedWheel as e:
raise UnsupportedWheel(f"{name} has an invalid wheel, {e}")
dist = pkg_resources.DistInfoDistribution(
location=wheel.location,
metadata=WheelMetadata(metadata_text, wheel.location),
project_name=name,
)
return cls(dist)
@property
@@ -49,41 +125,47 @@ class Distribution(BaseDistribution):
return self._dist.location
@property
def info_directory(self) -> Optional[str]:
def info_location(self) -> Optional[str]:
return self._dist.egg_info
@property
def canonical_name(self) -> "NormalizedName":
def installed_by_distutils(self) -> bool:
# A distutils-installed distribution is provided by FileMetadata. This
# provider has a "path" attribute not present anywhere else. Not the
# best introspection logic, but pip has been doing this for a long time.
try:
return bool(self._dist._provider.path)
except AttributeError:
return False
@property
def canonical_name(self) -> NormalizedName:
return canonicalize_name(self._dist.project_name)
@property
def version(self) -> DistributionVersion:
return parse_version(self._dist.version)
@property
def installer(self) -> str:
return get_installer(self._dist)
def is_file(self, path: InfoPath) -> bool:
return self._dist.has_metadata(str(path))
@property
def editable(self) -> bool:
return misc.dist_is_editable(self._dist)
@property
def local(self) -> bool:
return misc.dist_is_local(self._dist)
@property
def in_usersite(self) -> bool:
return misc.dist_in_usersite(self._dist)
@property
def in_site_packages(self) -> bool:
return misc.dist_in_site_packages(self._dist)
def read_text(self, name: str) -> str:
def iterdir(self, path: InfoPath) -> Iterator[pathlib.PurePosixPath]:
name = str(path)
if not self._dist.has_metadata(name):
raise FileNotFoundError(name)
return self._dist.get_metadata(name)
if not self._dist.isdir(name):
raise NotADirectoryError(name)
for child in self._dist.metadata_listdir(name):
yield pathlib.PurePosixPath(path, child)
def read_text(self, path: InfoPath) -> str:
name = str(path)
if not self._dist.has_metadata(name):
raise FileNotFoundError(name)
content = self._dist.get_metadata(name)
if content is None:
raise NoneMetadataError(self, name)
return content
def iter_entry_points(self) -> Iterable[BaseEntryPoint]:
for group, entries in self._dist.get_entry_map().items():
@@ -93,13 +175,35 @@ class Distribution(BaseDistribution):
@property
def metadata(self) -> email.message.Message:
return get_metadata(self._dist)
"""
:raises NoneMetadataError: if the distribution reports `has_metadata()`
True but `get_metadata()` returns None.
"""
if isinstance(self._dist, pkg_resources.DistInfoDistribution):
metadata_name = "METADATA"
else:
metadata_name = "PKG-INFO"
try:
metadata = self.read_text(metadata_name)
except FileNotFoundError:
if self.location:
displaying_path = display_path(self.location)
else:
displaying_path = repr(self.location)
logger.warning("No metadata found in %s", displaying_path)
metadata = ""
feed_parser = email.parser.FeedParser()
feed_parser.feed(metadata)
return feed_parser.close()
def iter_dependencies(self, extras: Collection[str] = ()) -> Iterable[Requirement]:
if extras: # pkg_resources raises on invalid extras, so we sanitize.
extras = frozenset(extras).intersection(self._dist.extras)
return self._dist.requires(extras)
def iter_provided_extras(self) -> Iterable[str]:
return self._dist.extras
class Environment(BaseEnvironment):
def __init__(self, ws: pkg_resources.WorkingSet) -> None:
@@ -126,7 +230,6 @@ class Environment(BaseEnvironment):
return None
def get_distribution(self, name: str) -> Optional[BaseDistribution]:
# Search the distribution by looking through the working set.
dist = self._search_distribution(name)
if dist:
@@ -5,8 +5,7 @@ from pipenv.patched.notpip._internal.utils.models import KeyBasedCompareMixin
class InstallationCandidate(KeyBasedCompareMixin):
"""Represents a potential "candidate" for installation.
"""
"""Represents a potential "candidate" for installation."""
__slots__ = ["name", "version", "link"]
@@ -17,15 +16,19 @@ class InstallationCandidate(KeyBasedCompareMixin):
super().__init__(
key=(self.name, self.version, self.link),
defining_class=InstallationCandidate
defining_class=InstallationCandidate,
)
def __repr__(self) -> str:
return "<InstallationCandidate({!r}, {!r}, {!r})>".format(
self.name, self.version, self.link,
self.name,
self.version,
self.link,
)
def __str__(self) -> str:
return '{!r} candidate (version {} at {})'.format(
self.name, self.version, self.link,
return "{!r} candidate (version {} at {})".format(
self.name,
self.version,
self.link,
)
@@ -137,9 +137,7 @@ class DirInfo:
def _from_dict(cls, d: Optional[Dict[str, Any]]) -> Optional["DirInfo"]:
if d is None:
return None
return cls(
editable=_get_required(d, bool, "editable", default=False)
)
return cls(editable=_get_required(d, bool, "editable", default=False))
def _to_dict(self) -> Dict[str, Any]:
return _filter_none(editable=self.editable or None)
@@ -149,7 +147,6 @@ InfoType = Union[ArchiveInfo, DirInfo, VcsInfo]
class DirectUrl:
def __init__(
self,
url: str,
@@ -165,9 +162,9 @@ class DirectUrl:
return netloc
user_pass, netloc_no_user_pass = netloc.split("@", 1)
if (
isinstance(self.info, VcsInfo) and
self.info.vcs == "git" and
user_pass == "git"
isinstance(self.info, VcsInfo)
and self.info.vcs == "git"
and user_pass == "git"
):
return netloc
if ENV_VAR_RE.match(user_pass):
@@ -218,3 +215,6 @@ class 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
@@ -6,15 +6,14 @@ from pipenv.patched.notpip._internal.exceptions import CommandError
class FormatControl:
"""Helper for managing formats from which a package can be installed.
"""
"""Helper for managing formats from which a package can be installed."""
__slots__ = ["no_binary", "only_binary"]
def __init__(
self,
no_binary: Optional[Set[str]] = None,
only_binary: Optional[Set[str]] = None
only_binary: Optional[Set[str]] = None,
) -> None:
if no_binary is None:
no_binary = set()
@@ -31,35 +30,30 @@ class FormatControl:
if self.__slots__ != other.__slots__:
return False
return all(
getattr(self, k) == getattr(other, k)
for k in self.__slots__
)
return all(getattr(self, k) == getattr(other, k) for k in self.__slots__)
def __repr__(self) -> str:
return "{}({}, {})".format(
self.__class__.__name__,
self.no_binary,
self.only_binary
self.__class__.__name__, self.no_binary, self.only_binary
)
@staticmethod
def handle_mutual_excludes(value: str, target: Set[str], other: Set[str]) -> None:
if value.startswith('-'):
if value.startswith("-"):
raise CommandError(
"--no-binary / --only-binary option requires 1 argument."
)
new = value.split(',')
while ':all:' in new:
new = value.split(",")
while ":all:" in new:
other.clear()
target.clear()
target.add(':all:')
del new[:new.index(':all:') + 1]
target.add(":all:")
del new[: new.index(":all:") + 1]
# Without a none, we want to discard everything as :all: covers it
if ':none:' not in new:
if ":none:" not in new:
return
for name in new:
if name == ':none:':
if name == ":none:":
target.clear()
continue
name = canonicalize_name(name)
@@ -69,16 +63,18 @@ class FormatControl:
def get_allowed_formats(self, canonical_name: str) -> FrozenSet[str]:
result = {"binary", "source"}
if canonical_name in self.only_binary:
result.discard('source')
result.discard("source")
elif canonical_name in self.no_binary:
result.discard('binary')
elif ':all:' in self.only_binary:
result.discard('source')
elif ':all:' in self.no_binary:
result.discard('binary')
result.discard("binary")
elif ":all:" in self.only_binary:
result.discard("source")
elif ":all:" in self.no_binary:
result.discard("binary")
return frozenset(result)
def disallow_binaries(self) -> None:
self.handle_mutual_excludes(
':all:', self.no_binary, self.only_binary,
":all:",
self.no_binary,
self.only_binary,
)
@@ -2,18 +2,16 @@ import urllib.parse
class PackageIndex:
"""Represents a Package Index and provides easier access to endpoints
"""
"""Represents a Package Index and provides easier access to endpoints"""
__slots__ = ['url', 'netloc', 'simple_url', 'pypi_url',
'file_storage_domain']
__slots__ = ["url", "netloc", "simple_url", "pypi_url", "file_storage_domain"]
def __init__(self, url: str, file_storage_domain: str) -> None:
super().__init__()
self.url = url
self.netloc = urllib.parse.urlsplit(url).netloc
self.simple_url = self._url_for_path('simple')
self.pypi_url = self._url_for_path('pypi')
self.simple_url = self._url_for_path("simple")
self.pypi_url = self._url_for_path("pypi")
# This is part of a temporary hack used to block installs of PyPI
# packages which depend on external urls only necessary until PyPI can
@@ -24,9 +22,7 @@ class PackageIndex:
return urllib.parse.urljoin(self.url, path)
PyPI = PackageIndex(
'https://pypi.org/', file_storage_domain='files.pythonhosted.org'
)
PyPI = PackageIndex("https://pypi.org/", file_storage_domain="files.pythonhosted.org")
TestPyPI = PackageIndex(
'https://test.pypi.org/', file_storage_domain='test-files.pythonhosted.org'
"https://test.pypi.org/", file_storage_domain="test-files.pythonhosted.org"
)
+17 -17
View File
@@ -26,8 +26,7 @@ _SUPPORTED_HASHES = ("sha1", "sha224", "sha384", "sha256", "sha512", "md5")
class Link(KeyBasedCompareMixin):
"""Represents a parsed link from a Package Index's simple URL
"""
"""Represents a parsed link from a Package Index's simple URL"""
__slots__ = [
"_parsed_url",
@@ -68,7 +67,7 @@ class Link(KeyBasedCompareMixin):
"""
# url can be a UNC windows share
if url.startswith('\\\\'):
if url.startswith("\\\\"):
url = path_to_url(url)
self._parsed_url = urllib.parse.urlsplit(url)
@@ -86,17 +85,18 @@ class Link(KeyBasedCompareMixin):
def __str__(self) -> str:
if self.requires_python:
rp = f' (requires-python:{self.requires_python})'
rp = f" (requires-python:{self.requires_python})"
else:
rp = ''
rp = ""
if self.comes_from:
return '{} (from {}){}'.format(
redact_auth_from_url(self._url), self.comes_from, rp)
return "{} (from {}){}".format(
redact_auth_from_url(self._url), self.comes_from, rp
)
else:
return redact_auth_from_url(str(self._url))
def __repr__(self) -> str:
return f'<Link {self}>'
return f"<Link {self}>"
@property
def url(self) -> str:
@@ -104,7 +104,7 @@ class Link(KeyBasedCompareMixin):
@property
def filename(self) -> str:
path = self.path.rstrip('/')
path = self.path.rstrip("/")
name = posixpath.basename(path)
if not name:
# Make sure we don't leak auth information if the netloc
@@ -113,7 +113,7 @@ class Link(KeyBasedCompareMixin):
return netloc
name = urllib.parse.unquote(name)
assert name, f'URL {self._url!r} produced no filename'
assert name, f"URL {self._url!r} produced no filename"
return name
@property
@@ -136,7 +136,7 @@ class Link(KeyBasedCompareMixin):
return urllib.parse.unquote(self._parsed_url.path)
def splitext(self) -> Tuple[str, str]:
return splitext(posixpath.basename(self.path.rstrip('/')))
return splitext(posixpath.basename(self.path.rstrip("/")))
@property
def ext(self) -> str:
@@ -145,9 +145,9 @@ class Link(KeyBasedCompareMixin):
@property
def url_without_fragment(self) -> str:
scheme, netloc, path, query, fragment = self._parsed_url
return urllib.parse.urlunsplit((scheme, netloc, path, query, ''))
return urllib.parse.urlunsplit((scheme, netloc, path, query, ""))
_egg_fragment_re = re.compile(r'[#&]egg=([^&]*)')
_egg_fragment_re = re.compile(r"[#&]egg=([^&]*)")
@property
def egg_fragment(self) -> Optional[str]:
@@ -156,7 +156,7 @@ class Link(KeyBasedCompareMixin):
return None
return match.group(1)
_subdirectory_fragment_re = re.compile(r'[#&]subdirectory=([^&]*)')
_subdirectory_fragment_re = re.compile(r"[#&]subdirectory=([^&]*)")
@property
def subdirectory_fragment(self) -> Optional[str]:
@@ -166,7 +166,7 @@ class Link(KeyBasedCompareMixin):
return match.group(1)
_hash_re = re.compile(
r'({choices})=([a-f0-9]+)'.format(choices="|".join(_SUPPORTED_HASHES))
r"({choices})=([a-f0-9]+)".format(choices="|".join(_SUPPORTED_HASHES))
)
@property
@@ -185,11 +185,11 @@ class Link(KeyBasedCompareMixin):
@property
def show_url(self) -> str:
return posixpath.basename(self._url.split('#', 1)[0].split('?', 1)[0])
return posixpath.basename(self._url.split("#", 1)[0].split("?", 1)[0])
@property
def is_file(self) -> bool:
return self.scheme == 'file'
return self.scheme == "file"
def is_existing_dir(self) -> bool:
return self.is_file and os.path.isdir(self.file_path)
@@ -6,7 +6,7 @@ https://docs.python.org/3/install/index.html#alternate-installation.
"""
SCHEME_KEYS = ['platlib', 'purelib', 'headers', 'scripts', 'data']
SCHEME_KEYS = ["platlib", "purelib", "headers", "scripts", "data"]
class Scheme:
@@ -39,7 +39,7 @@ class SearchScope:
# blindly normalize anything starting with a ~...
built_find_links: List[str] = []
for link in find_links:
if link.startswith('~'):
if link.startswith("~"):
new_link = normalize_path(link)
if os.path.exists(new_link):
link = new_link
@@ -50,11 +50,11 @@ class SearchScope:
if not has_tls():
for link in itertools.chain(index_urls, built_find_links):
parsed = urllib.parse.urlparse(link)
if parsed.scheme == 'https':
if parsed.scheme == "https":
logger.warning(
'pip is configured with locations that require '
'TLS/SSL, however the ssl module in Python is not '
'available.'
"pip is configured with locations that require "
"TLS/SSL, however the ssl module in Python is not "
"available."
)
break
@@ -92,20 +92,23 @@ class SearchScope:
# exceptions for malformed URLs
if not purl.scheme and not purl.netloc:
logger.warning(
'The index url "%s" seems invalid, '
'please provide a scheme.', redacted_index_url)
'The index url "%s" seems invalid, please provide a scheme.',
redacted_index_url,
)
redacted_index_urls.append(redacted_index_url)
lines.append('Looking in indexes: {}'.format(
', '.join(redacted_index_urls)))
lines.append(
"Looking in indexes: {}".format(", ".join(redacted_index_urls))
)
if self.find_links:
lines.append(
'Looking in links: {}'.format(', '.join(
redact_auth_from_url(url) for url in self.find_links))
"Looking in links: {}".format(
", ".join(redact_auth_from_url(url) for url in self.find_links)
)
)
return '\n'.join(lines)
return "\n".join(lines)
def get_index_urls_locations(self, project_name: str) -> List[str]:
"""Returns the locations found via self.index_urls
@@ -116,15 +119,15 @@ class SearchScope:
def mkurl_pypi_url(url: str) -> str:
loc = posixpath.join(
url,
urllib.parse.quote(canonicalize_name(project_name)))
url, urllib.parse.quote(canonicalize_name(project_name))
)
# For maximum compatibility with easy_install, ensure the path
# ends in a trailing slash. Although this isn't in the spec
# (and PyPI can handle it without the slash) some other index
# implementations might break if they relied on easy_install's
# behavior.
if not loc.endswith('/'):
loc = loc + '/'
if not loc.endswith("/"):
loc = loc + "/"
return loc
index_urls = self.index_urls
@@ -9,8 +9,13 @@ class SelectionPreferences:
and installing files.
"""
__slots__ = ['allow_yanked', 'allow_all_prereleases', 'format_control',
'prefer_binary', 'ignore_requires_python']
__slots__ = [
"allow_yanked",
"allow_all_prereleases",
"format_control",
"prefer_binary",
"ignore_requires_python",
]
# Don't include an allow_yanked default value to make sure each call
# site considers whether yanked releases are allowed. This also causes
@@ -53,7 +53,7 @@ class TargetPython:
else:
py_version_info = normalize_version_info(py_version_info)
py_version = '.'.join(map(str, py_version_info[:2]))
py_version = ".".join(map(str, py_version_info[:2]))
self.abis = abis
self.implementation = implementation
@@ -70,19 +70,18 @@ class TargetPython:
"""
display_version = None
if self._given_py_version_info is not None:
display_version = '.'.join(
display_version = ".".join(
str(part) for part in self._given_py_version_info
)
key_values = [
('platforms', self.platforms),
('version_info', display_version),
('abis', self.abis),
('implementation', self.implementation),
("platforms", self.platforms),
("version_info", display_version),
("abis", self.abis),
("implementation", self.implementation),
]
return ' '.join(
f'{key}={value!r}' for key, value in key_values
if value is not None
return " ".join(
f"{key}={value!r}" for key, value in key_values if value is not None
)
def get_tags(self) -> List[Tag]:
@@ -16,7 +16,7 @@ class Wheel:
r"""^(?P<namever>(?P<name>.+?)-(?P<ver>.*?))
((-(?P<build>\d[^-]*?))?-(?P<pyver>.+?)-(?P<abi>.+?)-(?P<plat>.+?)
\.whl|\.dist-info)$""",
re.VERBOSE
re.VERBOSE,
)
def __init__(self, filename: str) -> None:
@@ -25,23 +25,20 @@ class Wheel:
"""
wheel_info = self.wheel_file_re.match(filename)
if not wheel_info:
raise InvalidWheelFilename(
f"{filename} is not a valid wheel filename."
)
raise InvalidWheelFilename(f"{filename} is not a valid wheel filename.")
self.filename = filename
self.name = wheel_info.group('name').replace('_', '-')
self.name = wheel_info.group("name").replace("_", "-")
# we'll assume "_" means "-" due to wheel naming scheme
# (https://github.com/pypa/pip/issues/1150)
self.version = wheel_info.group('ver').replace('_', '-')
self.build_tag = wheel_info.group('build')
self.pyversions = wheel_info.group('pyver').split('.')
self.abis = wheel_info.group('abi').split('.')
self.plats = wheel_info.group('plat').split('.')
self.version = wheel_info.group("ver").replace("_", "-")
self.build_tag = wheel_info.group("build")
self.pyversions = wheel_info.group("pyver").split(".")
self.abis = wheel_info.group("abi").split(".")
self.plats = wheel_info.group("plat").split(".")
# All the tag combinations from this file
self.file_tags = {
Tag(x, y, z) for x in self.pyversions
for y in self.abis for z in self.plats
Tag(x, y, z) for x in self.pyversions for y in self.abis for z in self.plats
}
def get_formatted_file_tags(self) -> List[str]:
@@ -28,13 +28,13 @@ Credentials = Tuple[str, str, str]
try:
import keyring
except ImportError:
keyring = None
keyring = None # type: ignore[assignment]
except Exception as exc:
logger.warning(
"Keyring is skipped due to an exception: %s",
str(exc),
)
keyring = None
keyring = None # type: ignore[assignment]
def get_keyring_auth(url: Optional[str], username: Optional[str]) -> Optional[AuthInfo]:
@@ -66,7 +66,7 @@ def get_keyring_auth(url: Optional[str], username: Optional[str]) -> Optional[Au
"Keyring is skipped due to an exception: %s",
str(exc),
)
keyring = None
keyring = None # type: ignore[assignment]
return None
@@ -179,9 +179,16 @@ class MultiDomainBasicAuth(AuthBase):
# Try to get credentials from original url
username, password = self._get_new_credentials(original_url)
# If credentials not found, use any stored credentials for this netloc
if username is None and password is None:
username, password = self.passwords.get(netloc, (None, None))
# If credentials not found, use any stored credentials for this netloc.
# Do this if either the username or the password is missing.
# This accounts for the situation in which the user has specified
# the username in the index url, but the password comes from keyring.
if (username is None or password is None) and netloc in self.passwords:
un, pw = self.passwords[netloc]
# It is possible that the cached credentials are for a different username,
# in which case the cache should be ignored.
if username is None or username == un:
username, password = un, pw
if username is not None or password is not None:
# Convert the username and password if they're None, so that
@@ -53,7 +53,7 @@ class SafeFileCache(BaseCache):
with open(path, "rb") as f:
return f.read()
def set(self, key: str, value: bytes) -> None:
def set(self, key: str, value: bytes, expires: Optional[int] = None) -> None:
path = self._get_cache_path(key)
with suppressed_cache_errors():
ensure_dir(os.path.dirname(path))
@@ -8,7 +8,7 @@ from typing import Iterable, Optional, Tuple
from pipenv.patched.notpip._vendor.requests.models import CONTENT_CHUNK_SIZE, Response
from pipenv.patched.notpip._internal.cli.progress_bars import DownloadProgressProvider
from pipenv.patched.notpip._internal.cli.progress_bars import get_download_progress_renderer
from pipenv.patched.notpip._internal.exceptions import NetworkConnectionError
from pipenv.patched.notpip._internal.models.index import PyPI
from pipenv.patched.notpip._internal.models.link import Link
@@ -65,7 +65,8 @@ def _prepare_download(
if not show_progress:
return chunks
return DownloadProgressProvider(progress_bar, max=total_length)(chunks)
renderer = get_download_progress_renderer(bar_type=progress_bar, size=total_length)
return renderer(chunks)
def sanitize_content_filename(filename: str) -> str:
@@ -8,33 +8,33 @@ from tempfile import NamedTemporaryFile
from typing import Any, Dict, Iterator, List, Optional, Tuple
from zipfile import BadZipfile, ZipFile
from pipenv.patched.notpip._vendor.pkg_resources import Distribution
from pipenv.patched.notpip._vendor.packaging.utils import canonicalize_name
from pipenv.patched.notpip._vendor.requests.models import CONTENT_CHUNK_SIZE, Response
from pipenv.patched.notpip._internal.metadata import BaseDistribution, MemoryWheel, get_wheel_distribution
from pipenv.patched.notpip._internal.network.session import PipSession
from pipenv.patched.notpip._internal.network.utils import HEADERS, raise_for_status, response_chunks
from pipenv.patched.notpip._internal.utils.wheel import pkg_resources_distribution_for_wheel
class HTTPRangeRequestUnsupported(Exception):
pass
def dist_from_wheel_url(name: str, url: str, session: PipSession) -> Distribution:
"""Return a pkg_resources.Distribution from the given wheel URL.
def dist_from_wheel_url(name: str, url: str, session: PipSession) -> BaseDistribution:
"""Return a distribution object from the given wheel URL.
This uses HTTP range requests to only fetch the potion of the wheel
containing metadata, just enough for the object to be constructed.
If such requests are not supported, HTTPRangeRequestUnsupported
is raised.
"""
with LazyZipOverHTTP(url, session) as wheel:
with LazyZipOverHTTP(url, session) as zf:
# For read-only ZIP files, ZipFile only needs methods read,
# seek, seekable and tell, not the whole IO protocol.
zip_file = ZipFile(wheel) # type: ignore
wheel = MemoryWheel(zf.name, zf) # type: ignore
# After context manager exit, wheel.name
# is an invalid file by intention.
return pkg_resources_distribution_for_wheel(zip_file, name, wheel.name)
return get_wheel_distribution(wheel, canonicalize_name(name))
class LazyZipOverHTTP:
@@ -2,17 +2,8 @@
network request configuration and behavior.
"""
# When mypy runs on Windows the call to distro.linux_distribution() is skipped
# resulting in the failure:
#
# error: unused 'type: ignore' comment
#
# If the upstream module adds typing, this comment should be removed. See
# https://github.com/nir0s/distro/pull/269
#
# mypy: warn-unused-ignores=False
import email.utils
import io
import ipaddress
import json
import logging
@@ -128,9 +119,8 @@ def user_agent() -> str:
if sys.platform.startswith("linux"):
from pipenv.patched.notpip._vendor import distro
# https://github.com/nir0s/distro/pull/269
linux_distribution = distro.linux_distribution() # type: ignore
distro_infos = dict(
linux_distribution = distro.name(), distro.version(), distro.codename()
distro_infos: Dict[str, Any] = dict(
filter(
lambda x: x[1],
zip(["name", "version", "id"], linux_distribution),
@@ -218,8 +208,11 @@ class LocalFSAdapter(BaseAdapter):
try:
stats = os.stat(pathname)
except OSError as exc:
# format the exception raised as a io.BytesIO object,
# to return a better error message:
resp.status_code = 404
resp.raw = exc
resp.reason = type(exc).__name__
resp.raw = io.BytesIO(f"{resp.reason}: {exc}".encode("utf8"))
else:
modified = email.utils.formatdate(stats.st_mtime, usegmt=True)
content_type = mimetypes.guess_type(pathname)[0] or "text/plain"
@@ -369,8 +362,15 @@ class PipSession(requests.Session):
if host_port not in self.pip_trusted_origins:
self.pip_trusted_origins.append(host_port)
self.mount(
build_url_from_netloc(host, scheme="http") + "/", self._trusted_host_adapter
)
self.mount(build_url_from_netloc(host) + "/", self._trusted_host_adapter)
if not host_port[1]:
self.mount(
build_url_from_netloc(host, scheme="http") + ":",
self._trusted_host_adapter,
)
# Mount wildcard ports for the same host.
self.mount(build_url_from_netloc(host) + ":", self._trusted_host_adapter)
@@ -6,19 +6,22 @@ import os
from pipenv.patched.notpip._vendor.pep517.wrappers import Pep517HookCaller
from pipenv.patched.notpip._internal.build_env import BuildEnvironment
from pipenv.patched.notpip._internal.exceptions import (
InstallationSubprocessError,
MetadataGenerationFailed,
)
from pipenv.patched.notpip._internal.utils.subprocess import runner_with_spinner_message
from pipenv.patched.notpip._internal.utils.temp_dir import TempDirectory
def generate_metadata(build_env, backend):
# type: (BuildEnvironment, Pep517HookCaller) -> str
def generate_metadata(
build_env: BuildEnvironment, backend: Pep517HookCaller, details: str
) -> str:
"""Generate metadata using mechanisms described in PEP 517.
Returns the generated metadata directory.
"""
metadata_tmpdir = TempDirectory(
kind="modern-metadata", globally_managed=True
)
metadata_tmpdir = TempDirectory(kind="modern-metadata", globally_managed=True)
metadata_dir = metadata_tmpdir.path
@@ -26,10 +29,11 @@ def generate_metadata(build_env, backend):
# Note that Pep517HookCaller implements a fallback for
# prepare_metadata_for_build_wheel, so we don't have to
# consider the possibility that this hook doesn't exist.
runner = runner_with_spinner_message("Preparing wheel metadata")
runner = runner_with_spinner_message("Preparing metadata (pyproject.toml)")
with backend.subprocess_runner(runner):
distinfo_dir = backend.prepare_metadata_for_build_wheel(
metadata_dir
)
try:
distinfo_dir = backend.prepare_metadata_for_build_wheel(metadata_dir)
except InstallationSubprocessError as error:
raise MetadataGenerationFailed(package_details=details) from error
return os.path.join(metadata_dir, distinfo_dir)
@@ -0,0 +1,41 @@
"""Metadata generation logic for source distributions.
"""
import os
from pipenv.patched.notpip._vendor.pep517.wrappers import Pep517HookCaller
from pipenv.patched.notpip._internal.build_env import BuildEnvironment
from pipenv.patched.notpip._internal.exceptions import (
InstallationSubprocessError,
MetadataGenerationFailed,
)
from pipenv.patched.notpip._internal.utils.subprocess import runner_with_spinner_message
from pipenv.patched.notpip._internal.utils.temp_dir import TempDirectory
def generate_editable_metadata(
build_env: BuildEnvironment, backend: Pep517HookCaller, details: str
) -> str:
"""Generate metadata using mechanisms described in PEP 660.
Returns the generated metadata directory.
"""
metadata_tmpdir = TempDirectory(kind="modern-metadata", globally_managed=True)
metadata_dir = metadata_tmpdir.path
with build_env:
# Note that Pep517HookCaller implements a fallback for
# prepare_metadata_for_build_wheel/editable, so we don't have to
# consider the possibility that this hook doesn't exist.
runner = runner_with_spinner_message(
"Preparing editable metadata (pyproject.toml)"
)
with backend.subprocess_runner(runner):
try:
distinfo_dir = backend.prepare_metadata_for_build_editable(metadata_dir)
except InstallationSubprocessError as error:
raise MetadataGenerationFailed(package_details=details) from error
return os.path.join(metadata_dir, distinfo_dir)
@@ -5,7 +5,12 @@ import logging
import os
from pipenv.patched.notpip._internal.build_env import BuildEnvironment
from pipenv.patched.notpip._internal.exceptions import InstallationError
from pipenv.patched.notpip._internal.cli.spinners import open_spinner
from pipenv.patched.notpip._internal.exceptions import (
InstallationError,
InstallationSubprocessError,
MetadataGenerationFailed,
)
from pipenv.patched.notpip._internal.utils.setuptools_build import make_setuptools_egg_info_args
from pipenv.patched.notpip._internal.utils.subprocess import call_subprocess
from pipenv.patched.notpip._internal.utils.temp_dir import TempDirectory
@@ -13,49 +18,39 @@ from pipenv.patched.notpip._internal.utils.temp_dir import TempDirectory
logger = logging.getLogger(__name__)
def _find_egg_info(directory):
# type: (str) -> str
"""Find an .egg-info subdirectory in `directory`.
"""
filenames = [
f for f in os.listdir(directory) if f.endswith(".egg-info")
]
def _find_egg_info(directory: str) -> str:
"""Find an .egg-info subdirectory in `directory`."""
filenames = [f for f in os.listdir(directory) if f.endswith(".egg-info")]
if not filenames:
raise InstallationError(
f"No .egg-info directory found in {directory}"
)
raise InstallationError(f"No .egg-info directory found in {directory}")
if len(filenames) > 1:
raise InstallationError(
"More than one .egg-info directory found in {}".format(
directory
)
"More than one .egg-info directory found in {}".format(directory)
)
return os.path.join(directory, filenames[0])
def generate_metadata(
build_env, # type: BuildEnvironment
setup_py_path, # type: str
source_dir, # type: str
isolated, # type: bool
details, # type: str
):
# type: (...) -> str
build_env: BuildEnvironment,
setup_py_path: str,
source_dir: str,
isolated: bool,
details: str,
) -> str:
"""Generate metadata using setup.py-based defacto mechanisms.
Returns the generated metadata directory.
"""
logger.debug(
'Running setup.py (path:%s) egg_info for package %s',
setup_py_path, details,
"Running setup.py (path:%s) egg_info for package %s",
setup_py_path,
details,
)
egg_info_dir = TempDirectory(
kind="pip-egg-info", globally_managed=True
).path
egg_info_dir = TempDirectory(kind="pip-egg-info", globally_managed=True).path
args = make_setuptools_egg_info_args(
setup_py_path,
@@ -64,11 +59,16 @@ def generate_metadata(
)
with build_env:
call_subprocess(
args,
cwd=source_dir,
command_desc='python setup.py egg_info',
)
with open_spinner("Preparing metadata (setup.py)") as spinner:
try:
call_subprocess(
args,
cwd=source_dir,
command_desc="python setup.py egg_info",
spinner=spinner,
)
except InstallationSubprocessError as error:
raise MetadataGenerationFailed(package_details=details) from error
# Return the .egg-info directory.
return _find_egg_info(egg_info_dir)
@@ -10,22 +10,21 @@ logger = logging.getLogger(__name__)
def build_wheel_pep517(
name, # type: str
backend, # type: Pep517HookCaller
metadata_directory, # type: str
tempd, # type: str
):
# type: (...) -> Optional[str]
name: str,
backend: Pep517HookCaller,
metadata_directory: str,
tempd: str,
) -> Optional[str]:
"""Build one InstallRequirement using the PEP 517 build process.
Returns path to wheel if successfully built. Otherwise, returns None.
"""
assert metadata_directory is not None
try:
logger.debug('Destination directory: %s', tempd)
logger.debug("Destination directory: %s", tempd)
runner = runner_with_spinner_message(
f'Building wheel for {name} (PEP 517)'
f"Building wheel for {name} (pyproject.toml)"
)
with backend.subprocess_runner(runner):
wheel_name = backend.build_wheel(
@@ -33,6 +32,6 @@ def build_wheel_pep517(
metadata_directory=metadata_directory,
)
except Exception:
logger.error('Failed building wheel for %s', name)
logger.error("Failed building wheel for %s", name)
return None
return os.path.join(tempd, wheel_name)
@@ -0,0 +1,46 @@
import logging
import os
from typing import Optional
from pipenv.patched.notpip._vendor.pep517.wrappers import HookMissing, Pep517HookCaller
from pipenv.patched.notpip._internal.utils.subprocess import runner_with_spinner_message
logger = logging.getLogger(__name__)
def build_wheel_editable(
name: str,
backend: Pep517HookCaller,
metadata_directory: str,
tempd: str,
) -> Optional[str]:
"""Build one InstallRequirement using the PEP 660 build process.
Returns path to wheel if successfully built. Otherwise, returns None.
"""
assert metadata_directory is not None
try:
logger.debug("Destination directory: %s", tempd)
runner = runner_with_spinner_message(
f"Building editable for {name} (pyproject.toml)"
)
with backend.subprocess_runner(runner):
try:
wheel_name = backend.build_editable(
tempd,
metadata_directory=metadata_directory,
)
except HookMissing as e:
logger.error(
"Cannot build editable %s because the build "
"backend does not have the %s hook",
name,
e,
)
return None
except Exception:
logger.error("Failed building editable for %s", name)
return None
return os.path.join(tempd, wheel_name)
@@ -4,59 +4,51 @@ from typing import List, Optional
from pipenv.patched.notpip._internal.cli.spinners import open_spinner
from pipenv.patched.notpip._internal.utils.setuptools_build import make_setuptools_bdist_wheel_args
from pipenv.patched.notpip._internal.utils.subprocess import (
LOG_DIVIDER,
call_subprocess,
format_command_args,
)
from pipenv.patched.notpip._internal.utils.subprocess import call_subprocess, format_command_args
logger = logging.getLogger(__name__)
def format_command_result(
command_args, # type: List[str]
command_output, # type: str
):
# type: (...) -> str
command_args: List[str],
command_output: str,
) -> str:
"""Format command information for logging."""
command_desc = format_command_args(command_args)
text = f'Command arguments: {command_desc}\n'
text = f"Command arguments: {command_desc}\n"
if not command_output:
text += 'Command output: None'
text += "Command output: None"
elif logger.getEffectiveLevel() > logging.DEBUG:
text += 'Command output: [use --verbose to show]'
text += "Command output: [use --verbose to show]"
else:
if not command_output.endswith('\n'):
command_output += '\n'
text += f'Command output:\n{command_output}{LOG_DIVIDER}'
if not command_output.endswith("\n"):
command_output += "\n"
text += f"Command output:\n{command_output}"
return text
def get_legacy_build_wheel_path(
names, # type: List[str]
temp_dir, # type: str
name, # type: str
command_args, # type: List[str]
command_output, # type: str
):
# type: (...) -> Optional[str]
names: List[str],
temp_dir: str,
name: str,
command_args: List[str],
command_output: str,
) -> Optional[str]:
"""Return the path to the wheel in the temporary build directory."""
# Sort for determinism.
names = sorted(names)
if not names:
msg = (
'Legacy build of wheel for {!r} created no files.\n'
).format(name)
msg = ("Legacy build of wheel for {!r} created no files.\n").format(name)
msg += format_command_result(command_args, command_output)
logger.warning(msg)
return None
if len(names) > 1:
msg = (
'Legacy build of wheel for {!r} created more than one file.\n'
'Filenames (choosing first): {}\n'
"Legacy build of wheel for {!r} created more than one file.\n"
"Filenames (choosing first): {}\n"
).format(name, names)
msg += format_command_result(command_args, command_output)
logger.warning(msg)
@@ -65,14 +57,13 @@ def get_legacy_build_wheel_path(
def build_wheel_legacy(
name, # type: str
setup_py_path, # type: str
source_dir, # type: str
global_options, # type: List[str]
build_options, # type: List[str]
tempd, # type: str
):
# type: (...) -> Optional[str]
name: str,
setup_py_path: str,
source_dir: str,
global_options: List[str],
build_options: List[str],
tempd: str,
) -> Optional[str]:
"""Build one unpacked package using the "legacy" build process.
Returns path to wheel if successfully built. Otherwise, returns None.
@@ -84,19 +75,20 @@ def build_wheel_legacy(
destination_dir=tempd,
)
spin_message = f'Building wheel for {name} (setup.py)'
spin_message = f"Building wheel for {name} (setup.py)"
with open_spinner(spin_message) as spinner:
logger.debug('Destination directory: %s', tempd)
logger.debug("Destination directory: %s", tempd)
try:
output = call_subprocess(
wheel_args,
command_desc="python setup.py bdist_wheel",
cwd=source_dir,
spinner=spinner,
)
except Exception:
spinner.finish("error")
logger.error('Failed building wheel for %s', name)
logger.error("Failed building wheel for %s", name)
return None
names = os.listdir(tempd)
@@ -2,19 +2,16 @@
"""
import logging
from typing import TYPE_CHECKING, Callable, Dict, List, NamedTuple, Optional, Set, Tuple
from typing import Callable, Dict, List, NamedTuple, Optional, Set, Tuple
from pipenv.patched.notpip._vendor.packaging.requirements import Requirement
from pipenv.patched.notpip._vendor.packaging.utils import canonicalize_name
from pipenv.patched.notpip._vendor.packaging.utils import NormalizedName, canonicalize_name
from pipenv.patched.notpip._internal.distributions import make_distribution_for_install_requirement
from pipenv.patched.notpip._internal.metadata import get_default_environment
from pipenv.patched.notpip._internal.metadata.base import DistributionVersion
from pipenv.patched.notpip._internal.req.req_install import InstallRequirement
if TYPE_CHECKING:
from pipenv.patched.notpip._vendor.packaging.utils import NormalizedName
logger = logging.getLogger(__name__)
@@ -24,12 +21,12 @@ class PackageDetails(NamedTuple):
# Shorthands
PackageSet = Dict['NormalizedName', PackageDetails]
Missing = Tuple['NormalizedName', Requirement]
Conflicting = Tuple['NormalizedName', DistributionVersion, Requirement]
PackageSet = Dict[NormalizedName, PackageDetails]
Missing = Tuple[NormalizedName, Requirement]
Conflicting = Tuple[NormalizedName, DistributionVersion, Requirement]
MissingDict = Dict['NormalizedName', List[Missing]]
ConflictingDict = Dict['NormalizedName', List[Conflicting]]
MissingDict = Dict[NormalizedName, List[Missing]]
ConflictingDict = Dict[NormalizedName, List[Conflicting]]
CheckResult = Tuple[MissingDict, ConflictingDict]
ConflictDetails = Tuple[PackageSet, CheckResult]
@@ -51,8 +48,9 @@ def create_package_set_from_installed() -> Tuple[PackageSet, bool]:
return package_set, problems
def check_package_set(package_set, should_ignore=None):
# type: (PackageSet, Optional[Callable[[str], bool]]) -> CheckResult
def check_package_set(
package_set: PackageSet, should_ignore: Optional[Callable[[str], bool]] = None
) -> CheckResult:
"""Check if a package set is consistent
If should_ignore is passed, it should be a callable that takes a
@@ -64,8 +62,8 @@ def check_package_set(package_set, should_ignore=None):
for package_name, package_detail in package_set.items():
# Info about dependencies of package_name
missing_deps = set() # type: Set[Missing]
conflicting_deps = set() # type: Set[Conflicting]
missing_deps: Set[Missing] = set()
conflicting_deps: Set[Conflicting] = set()
if should_ignore and should_ignore(package_name):
continue
@@ -95,8 +93,7 @@ def check_package_set(package_set, should_ignore=None):
return missing, conflicting
def check_install_conflicts(to_install):
# type: (List[InstallRequirement]) -> ConflictDetails
def check_install_conflicts(to_install: List[InstallRequirement]) -> ConflictDetails:
"""For checking if the dependency graph would be consistent after \
installing given requirements
"""
@@ -112,33 +109,32 @@ def check_install_conflicts(to_install):
package_set,
check_package_set(
package_set, should_ignore=lambda name: name not in whitelist
)
),
)
def _simulate_installation_of(to_install, package_set):
# type: (List[InstallRequirement], PackageSet) -> Set[NormalizedName]
"""Computes the version of packages after installing to_install.
"""
def _simulate_installation_of(
to_install: List[InstallRequirement], package_set: PackageSet
) -> Set[NormalizedName]:
"""Computes the version of packages after installing to_install."""
# Keep track of packages that were installed
installed = set()
# Modify it as installing requirement_set would (assuming no errors)
for inst_req in to_install:
abstract_dist = make_distribution_for_install_requirement(inst_req)
dist = abstract_dist.get_pkg_resources_distribution()
assert dist is not None
name = canonicalize_name(dist.project_name)
package_set[name] = PackageDetails(dist.parsed_version, dist.requires())
dist = abstract_dist.get_metadata_distribution()
name = dist.canonical_name
package_set[name] = PackageDetails(dist.version, list(dist.iter_dependencies()))
installed.add(name)
return installed
def _create_whitelist(would_be_installed, package_set):
# type: (Set[NormalizedName], PackageSet) -> Set[NormalizedName]
def _create_whitelist(
would_be_installed: Set[NormalizedName], package_set: PackageSet
) -> Set[NormalizedName]:
packages_affected = set(would_be_installed)
for package_name in package_set:
@@ -1,19 +1,8 @@
import collections
import logging
import os
from typing import (
Container,
Dict,
Iterable,
Iterator,
List,
NamedTuple,
Optional,
Set,
Union,
)
from typing import Container, Dict, Iterable, Iterator, List, NamedTuple, Optional, Set
from pipenv.patched.notpip._vendor.packaging.requirements import Requirement
from pipenv.patched.notpip._vendor.packaging.utils import canonicalize_name
from pipenv.patched.notpip._vendor.packaging.version import Version
@@ -30,22 +19,20 @@ logger = logging.getLogger(__name__)
class _EditableInfo(NamedTuple):
requirement: Optional[str]
editable: bool
requirement: str
comments: List[str]
def freeze(
requirement=None, # type: Optional[List[str]]
local_only=False, # type: bool
user_only=False, # type: bool
paths=None, # type: Optional[List[str]]
isolated=False, # type: bool
exclude_editable=False, # type: bool
skip=() # type: Container[str]
):
# type: (...) -> Iterator[str]
installations = {} # type: Dict[str, FrozenRequirement]
requirement: Optional[List[str]] = None,
local_only: bool = False,
user_only: bool = False,
paths: Optional[List[str]] = None,
isolated: bool = False,
exclude_editable: bool = False,
skip: Container[str] = (),
) -> Iterator[str]:
installations: Dict[str, FrozenRequirement] = {}
dists = get_environment(paths).iter_installed_distributions(
local_only=local_only,
@@ -63,42 +50,50 @@ def freeze(
# should only be emitted once, even if the same option is in multiple
# requirements files, so we need to keep track of what has been emitted
# so that we don't emit it again if it's seen again
emitted_options = set() # type: Set[str]
emitted_options: Set[str] = set()
# keep track of which files a requirement is in so that we can
# give an accurate warning if a requirement appears multiple times.
req_files = collections.defaultdict(list) # type: Dict[str, List[str]]
req_files: Dict[str, List[str]] = collections.defaultdict(list)
for req_file_path in requirement:
with open(req_file_path) as req_file:
for line in req_file:
if (not line.strip() or
line.strip().startswith('#') or
line.startswith((
'-r', '--requirement',
'-f', '--find-links',
'-i', '--index-url',
'--pre',
'--trusted-host',
'--process-dependency-links',
'--extra-index-url',
'--use-feature'))):
if (
not line.strip()
or line.strip().startswith("#")
or line.startswith(
(
"-r",
"--requirement",
"-f",
"--find-links",
"-i",
"--index-url",
"--pre",
"--trusted-host",
"--process-dependency-links",
"--extra-index-url",
"--use-feature",
)
)
):
line = line.rstrip()
if line not in emitted_options:
emitted_options.add(line)
yield line
continue
if line.startswith('-e') or line.startswith('--editable'):
if line.startswith('-e'):
if line.startswith("-e") or line.startswith("--editable"):
if line.startswith("-e"):
line = line[2:].strip()
else:
line = line[len('--editable'):].strip().lstrip('=')
line = line[len("--editable") :].strip().lstrip("=")
line_req = install_req_from_editable(
line,
isolated=isolated,
)
else:
line_req = install_req_from_line(
COMMENT_RE.sub('', line).strip(),
COMMENT_RE.sub("", line).strip(),
isolated=isolated,
)
@@ -106,15 +101,15 @@ def freeze(
logger.info(
"Skipping line in requirement file [%s] because "
"it's not clear what it would install: %s",
req_file_path, line.strip(),
req_file_path,
line.strip(),
)
logger.info(
" (add #egg=PackageName to the URL to avoid"
" this warning)"
)
else:
line_req_canonical_name = canonicalize_name(
line_req.name)
line_req_canonical_name = canonicalize_name(line_req.name)
if line_req_canonical_name not in installations:
# either it's not installed, or it is installed
# but has been processed already
@@ -123,14 +118,13 @@ def freeze(
"Requirement file [%s] contains %s, but "
"package %r is not installed",
req_file_path,
COMMENT_RE.sub('', line).strip(),
line_req.name
COMMENT_RE.sub("", line).strip(),
line_req.name,
)
else:
req_files[line_req.name].append(req_file_path)
else:
yield str(installations[
line_req_canonical_name]).rstrip()
yield str(installations[line_req_canonical_name]).rstrip()
del installations[line_req_canonical_name]
req_files[line_req.name].append(req_file_path)
@@ -138,15 +132,14 @@ def freeze(
# single requirements file or in different requirements files).
for name, files in req_files.items():
if len(files) > 1:
logger.warning("Requirement %s included multiple times [%s]",
name, ', '.join(sorted(set(files))))
logger.warning(
"Requirement %s included multiple times [%s]",
name,
", ".join(sorted(set(files))),
)
yield(
'## The following requirements were added by '
'pip freeze:'
)
for installation in sorted(
installations.values(), key=lambda x: x.name.lower()):
yield ("## The following requirements were added by pip freeze:")
for installation in sorted(installations.values(), key=lambda x: x.name.lower()):
if installation.canonical_name not in skip:
yield str(installation).rstrip()
@@ -159,21 +152,12 @@ def _format_as_name_version(dist: BaseDistribution) -> str:
def _get_editable_info(dist: BaseDistribution) -> _EditableInfo:
"""
Compute and return values (req, editable, comments) for use in
Compute and return values (req, comments) for use in
FrozenRequirement.from_dist().
"""
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 pipenv.patched.notpip._internal.vcs import RemoteNotFoundError, RemoteNotValidError, vcs
@@ -182,13 +166,13 @@ def _get_editable_info(dist: BaseDistribution) -> _EditableInfo:
if vcs_backend is None:
display = _format_as_name_version(dist)
logger.debug(
'No VCS found for editable requirement "%s" in: %r', display,
'No VCS found for editable requirement "%s" in: %r',
display,
location,
)
return _EditableInfo(
requirement=location,
editable=True,
comments=[f'# Editable install with no version control ({display})'],
comments=[f"# Editable install with no version control ({display})"],
)
vcs_name = type(vcs_backend).__name__
@@ -199,50 +183,47 @@ def _get_editable_info(dist: BaseDistribution) -> _EditableInfo:
display = _format_as_name_version(dist)
return _EditableInfo(
requirement=location,
editable=True,
comments=[f'# Editable {vcs_name} install with no remote ({display})'],
comments=[f"# Editable {vcs_name} install with no remote ({display})"],
)
except RemoteNotValidError as ex:
display = _format_as_name_version(dist)
return _EditableInfo(
requirement=location,
editable=True,
comments=[
f"# Editable {vcs_name} install ({display}) with either a deleted "
f"local remote or invalid URI:",
f"# '{ex.url}'",
],
)
except BadCommand:
logger.warning(
'cannot determine version of editable source in %s '
'(%s command not found in path)',
"cannot determine version of editable source in %s "
"(%s command not found in path)",
location,
vcs_backend.name,
)
return _EditableInfo(requirement=None, editable=True, comments=[])
return _EditableInfo(requirement=location, comments=[])
except InstallationError as exc:
logger.warning(
"Error when trying to get requirement for VCS system %s, "
"falling back to uneditable format", exc
)
logger.warning("Error when trying to get requirement for VCS system %s", exc)
else:
return _EditableInfo(requirement=req, editable=True, comments=[])
return _EditableInfo(requirement=req, comments=[])
logger.warning('Could not determine repository location of %s', location)
logger.warning("Could not determine repository location of %s", location)
return _EditableInfo(
requirement=None,
editable=False,
comments=['## !! Could not determine repository location'],
requirement=location,
comments=["## !! Could not determine repository location"],
)
class FrozenRequirement:
def __init__(self, name, req, editable, comments=()):
# type: (str, Union[str, Requirement], bool, Iterable[str]) -> None
def __init__(
self,
name: str,
req: str,
editable: bool,
comments: Iterable[str] = (),
) -> None:
self.name = name
self.canonical_name = canonicalize_name(name)
self.req = req
@@ -251,27 +232,23 @@ class FrozenRequirement:
@classmethod
def from_dist(cls, dist: BaseDistribution) -> "FrozenRequirement":
# TODO `get_requirement_info` is taking care of editable requirements.
# TODO This should be refactored when we will add detection of
# editable that provide .dist-info metadata.
req, editable, comments = _get_editable_info(dist)
if req is None and not editable:
# if PEP 610 metadata is present, attempt to use it
editable = dist.editable
if editable:
req, comments = _get_editable_info(dist)
else:
comments = []
direct_url = dist.direct_url
if direct_url:
req = direct_url_as_pep440_direct_reference(
direct_url, dist.raw_name
)
comments = []
if req is None:
# name==version requirement
req = _format_as_name_version(dist)
# if PEP 610 metadata is present, use it
req = direct_url_as_pep440_direct_reference(direct_url, dist.raw_name)
else:
# name==version requirement
req = _format_as_name_version(dist)
return cls(dist.raw_name, req, editable, comments=comments)
def __str__(self):
# type: () -> str
def __str__(self) -> str:
req = self.req
if self.editable:
req = f'-e {req}'
return '\n'.join(list(self.comments) + [str(req)]) + '\n'
req = f"-e {req}"
return "\n".join(list(self.comments) + [str(req)]) + "\n"
@@ -12,22 +12,21 @@ logger = logging.getLogger(__name__)
def install_editable(
install_options, # type: List[str]
global_options, # type: Sequence[str]
prefix, # type: Optional[str]
home, # type: Optional[str]
use_user_site, # type: bool
name, # type: str
setup_py_path, # type: str
isolated, # type: bool
build_env, # type: BuildEnvironment
unpacked_source_directory, # type: str
):
# type: (...) -> None
install_options: List[str],
global_options: Sequence[str],
prefix: Optional[str],
home: Optional[str],
use_user_site: bool,
name: str,
setup_py_path: str,
isolated: bool,
build_env: BuildEnvironment,
unpacked_source_directory: str,
) -> None:
"""Install a package in editable mode. Most arguments are pass-through
to setuptools.
"""
logger.info('Running setup.py develop for %s', name)
logger.info("Running setup.py develop for %s", name)
args = make_setuptools_develop_args(
setup_py_path,
@@ -43,5 +42,6 @@ def install_editable(
with build_env:
call_subprocess(
args,
command_desc="python setup.py develop",
cwd=unpacked_source_directory,
)
@@ -3,14 +3,12 @@
import logging
import os
import sys
from distutils.util import change_root
from typing import List, Optional, Sequence
from pipenv.patched.notpip._internal.build_env import BuildEnvironment
from pipenv.patched.notpip._internal.exceptions import InstallationError
from pipenv.patched.notpip._internal.exceptions import InstallationError, LegacyInstallFailure
from pipenv.patched.notpip._internal.models.scheme import Scheme
from pipenv.patched.notpip._internal.utils.logging import indent_log
from pipenv.patched.notpip._internal.utils.misc import ensure_dir
from pipenv.patched.notpip._internal.utils.setuptools_build import make_setuptools_install_args
from pipenv.patched.notpip._internal.utils.subprocess import runner_with_spinner_message
@@ -19,19 +17,12 @@ from pipenv.patched.notpip._internal.utils.temp_dir import TempDirectory
logger = logging.getLogger(__name__)
class LegacyInstallFailure(Exception):
def __init__(self):
# type: () -> None
self.parent = sys.exc_info()
def write_installed_files_from_setuptools_record(
record_lines: List[str],
root: Optional[str],
req_description: str,
) -> None:
def prepend_root(path):
# type: (str) -> str
def prepend_root(path: str) -> str:
if root is None or not os.path.isabs(path):
return path
else:
@@ -39,7 +30,7 @@ def write_installed_files_from_setuptools_record(
for line in record_lines:
directory = os.path.dirname(line)
if directory.endswith('.egg-info'):
if directory.endswith(".egg-info"):
egg_info_dir = prepend_root(directory)
break
else:
@@ -55,39 +46,36 @@ def write_installed_files_from_setuptools_record(
filename = line.strip()
if os.path.isdir(filename):
filename += os.path.sep
new_lines.append(
os.path.relpath(prepend_root(filename), egg_info_dir)
)
new_lines.append(os.path.relpath(prepend_root(filename), egg_info_dir))
new_lines.sort()
ensure_dir(egg_info_dir)
inst_files_path = os.path.join(egg_info_dir, 'installed-files.txt')
with open(inst_files_path, 'w') as f:
f.write('\n'.join(new_lines) + '\n')
inst_files_path = os.path.join(egg_info_dir, "installed-files.txt")
with open(inst_files_path, "w") as f:
f.write("\n".join(new_lines) + "\n")
def install(
install_options, # type: List[str]
global_options, # type: Sequence[str]
root, # type: Optional[str]
home, # type: Optional[str]
prefix, # type: Optional[str]
use_user_site, # type: bool
pycompile, # type: bool
scheme, # type: Scheme
setup_py_path, # type: str
isolated, # type: bool
req_name, # type: str
build_env, # type: BuildEnvironment
unpacked_source_directory, # type: str
req_description, # type: str
):
# type: (...) -> bool
install_options: List[str],
global_options: Sequence[str],
root: Optional[str],
home: Optional[str],
prefix: Optional[str],
use_user_site: bool,
pycompile: bool,
scheme: Scheme,
setup_py_path: str,
isolated: bool,
req_name: str,
build_env: BuildEnvironment,
unpacked_source_directory: str,
req_description: str,
) -> bool:
header_dir = scheme.headers
with TempDirectory(kind="record") as temp_dir:
try:
record_filename = os.path.join(temp_dir.path, 'install-record.txt')
record_filename = os.path.join(temp_dir.path, "install-record.txt")
install_args = make_setuptools_install_args(
setup_py_path,
global_options=global_options,
@@ -105,20 +93,20 @@ def install(
runner = runner_with_spinner_message(
f"Running setup.py install for {req_name}"
)
with indent_log(), build_env:
with build_env:
runner(
cmd=install_args,
cwd=unpacked_source_directory,
)
if not os.path.exists(record_filename):
logger.debug('Record file %s not found', record_filename)
logger.debug("Record file %s not found", record_filename)
# Signal to the caller that we didn't install the new package
return False
except Exception:
except Exception as e:
# Signal to the caller that we didn't install the new package
raise LegacyInstallFailure
raise LegacyInstallFailure(package_details=req_name) from e
# At this point, we have successfully installed the requirement.
@@ -38,11 +38,14 @@ from zipfile import ZipFile, ZipInfo
from pipenv.patched.notpip._vendor.distlib.scripts import ScriptMaker
from pipenv.patched.notpip._vendor.distlib.util import get_export_entry
from pipenv.patched.notpip._vendor.packaging.utils import canonicalize_name
from pipenv.patched.notpip._vendor.six import ensure_str, ensure_text, reraise
from pipenv.patched.notpip._internal.exceptions import InstallationError
from pipenv.patched.notpip._internal.locations import get_major_minor_version
from pipenv.patched.notpip._internal.metadata import BaseDistribution, get_wheel_distribution
from pipenv.patched.notpip._internal.metadata import (
BaseDistribution,
FilesystemWheel,
get_wheel_distribution,
)
from pipenv.patched.notpip._internal.models.direct_url import DIRECT_URL_METADATA_NAME, DirectUrl
from pipenv.patched.notpip._internal.models.scheme import SCHEME_KEYS, Scheme
from pipenv.patched.notpip._internal.utils.filesystem import adjacent_tmp_file, replace
@@ -59,62 +62,55 @@ if TYPE_CHECKING:
from typing import Protocol
class File(Protocol):
src_record_path = None # type: RecordPath
dest_path = None # type: str
changed = None # type: bool
src_record_path: "RecordPath"
dest_path: str
changed: bool
def save(self):
# type: () -> None
def save(self) -> None:
pass
logger = logging.getLogger(__name__)
RecordPath = NewType('RecordPath', str)
RecordPath = NewType("RecordPath", str)
InstalledCSVRow = Tuple[RecordPath, str, Union[int, str]]
def rehash(path, blocksize=1 << 20):
# type: (str, int) -> Tuple[str, str]
def rehash(path: str, blocksize: int = 1 << 20) -> Tuple[str, str]:
"""Return (encoded_digest, length) for path using hashlib.sha256()"""
h, length = hash_file(path, blocksize)
digest = 'sha256=' + urlsafe_b64encode(
h.digest()
).decode('latin1').rstrip('=')
digest = "sha256=" + urlsafe_b64encode(h.digest()).decode("latin1").rstrip("=")
return (digest, str(length))
def csv_io_kwargs(mode):
# type: (str) -> Dict[str, Any]
def csv_io_kwargs(mode: str) -> Dict[str, Any]:
"""Return keyword arguments to properly open a CSV file
in the given mode.
"""
return {'mode': mode, 'newline': '', 'encoding': 'utf-8'}
return {"mode": mode, "newline": "", "encoding": "utf-8"}
def fix_script(path):
# type: (str) -> bool
def fix_script(path: str) -> bool:
"""Replace #!python with #!/path/to/python
Return True if file was changed.
"""
# XXX RECORD hashes will need to be updated
assert os.path.isfile(path)
with open(path, 'rb') as script:
with open(path, "rb") as script:
firstline = script.readline()
if not firstline.startswith(b'#!python'):
if not firstline.startswith(b"#!python"):
return False
exename = sys.executable.encode(sys.getfilesystemencoding())
firstline = b'#!' + exename + os.linesep.encode("ascii")
firstline = b"#!" + exename + os.linesep.encode("ascii")
rest = script.read()
with open(path, 'wb') as script:
with open(path, "wb") as script:
script.write(firstline)
script.write(rest)
return True
def wheel_root_is_purelib(metadata):
# type: (Message) -> bool
def wheel_root_is_purelib(metadata: Message) -> bool:
return metadata.get("Root-Is-Purelib", "").lower() == "true"
@@ -129,8 +125,7 @@ def get_entrypoints(dist: BaseDistribution) -> Tuple[Dict[str, str], Dict[str, s
return console_scripts, gui_scripts
def message_about_scripts_not_on_PATH(scripts):
# type: (Sequence[str]) -> Optional[str]
def message_about_scripts_not_on_PATH(scripts: Sequence[str]) -> Optional[str]:
"""Determine if any scripts are not on PATH and format a warning.
Returns a warning message if one or more scripts are not on PATH,
otherwise None.
@@ -139,7 +134,7 @@ def message_about_scripts_not_on_PATH(scripts):
return None
# Group scripts by the path they were installed in
grouped_by_dir = collections.defaultdict(set) # type: Dict[str, Set[str]]
grouped_by_dir: Dict[str, Set[str]] = collections.defaultdict(set)
for destfile in scripts:
parent_dir = os.path.dirname(destfile)
script_name = os.path.basename(destfile)
@@ -147,23 +142,24 @@ def message_about_scripts_not_on_PATH(scripts):
# We don't want to warn for directories that are on PATH.
not_warn_dirs = [
os.path.normcase(i).rstrip(os.sep) for i in
os.environ.get("PATH", "").split(os.pathsep)
os.path.normcase(i).rstrip(os.sep)
for i in os.environ.get("PATH", "").split(os.pathsep)
]
# If an executable sits with sys.executable, we don't warn for it.
# This covers the case of venv invocations without activating the venv.
not_warn_dirs.append(os.path.normcase(os.path.dirname(sys.executable)))
warn_for = {
parent_dir: scripts for parent_dir, scripts in grouped_by_dir.items()
warn_for: Dict[str, Set[str]] = {
parent_dir: scripts
for parent_dir, scripts in grouped_by_dir.items()
if os.path.normcase(parent_dir) not in not_warn_dirs
} # type: Dict[str, Set[str]]
}
if not warn_for:
return None
# Format a message
msg_lines = []
for parent_dir, dir_scripts in warn_for.items():
sorted_scripts = sorted(dir_scripts) # type: List[str]
sorted_scripts: List[str] = sorted(dir_scripts)
if len(sorted_scripts) == 1:
start_text = "script {} is".format(sorted_scripts[0])
else:
@@ -172,8 +168,9 @@ def message_about_scripts_not_on_PATH(scripts):
)
msg_lines.append(
"The {} installed in '{}' which is not on PATH."
.format(start_text, parent_dir)
"The {} installed in '{}' which is not on PATH.".format(
start_text, parent_dir
)
)
last_line_fmt = (
@@ -200,8 +197,9 @@ def message_about_scripts_not_on_PATH(scripts):
return "\n".join(msg_lines)
def _normalized_outrows(outrows):
# type: (Iterable[InstalledCSVRow]) -> List[Tuple[str, str, str]]
def _normalized_outrows(
outrows: Iterable[InstalledCSVRow],
) -> List[Tuple[str, str, str]]:
"""Normalize the given rows of a RECORD file.
Items in each row are converted into str. Rows are then sorted to make
@@ -221,69 +219,60 @@ def _normalized_outrows(outrows):
# For additional background, see--
# https://github.com/pypa/pip/issues/5868
return sorted(
(ensure_str(record_path, encoding='utf-8'), hash_, str(size))
for record_path, hash_, size in outrows
(record_path, hash_, str(size)) for record_path, hash_, size in outrows
)
def _record_to_fs_path(record_path):
# type: (RecordPath) -> str
def _record_to_fs_path(record_path: RecordPath) -> str:
return record_path
def _fs_to_record_path(path, relative_to=None):
# type: (str, Optional[str]) -> RecordPath
def _fs_to_record_path(path: str, relative_to: Optional[str] = None) -> RecordPath:
if relative_to is not None:
# On Windows, do not handle relative paths if they belong to different
# logical disks
if os.path.splitdrive(path)[0].lower() == \
os.path.splitdrive(relative_to)[0].lower():
if (
os.path.splitdrive(path)[0].lower()
== os.path.splitdrive(relative_to)[0].lower()
):
path = os.path.relpath(path, relative_to)
path = path.replace(os.path.sep, '/')
return cast('RecordPath', path)
def _parse_record_path(record_column):
# type: (str) -> RecordPath
p = ensure_text(record_column, encoding='utf-8')
return cast('RecordPath', p)
path = path.replace(os.path.sep, "/")
return cast("RecordPath", path)
def get_csv_rows_for_installed(
old_csv_rows, # type: List[List[str]]
installed, # type: Dict[RecordPath, RecordPath]
changed, # type: Set[RecordPath]
generated, # type: List[str]
lib_dir, # type: str
):
# type: (...) -> List[InstalledCSVRow]
old_csv_rows: List[List[str]],
installed: Dict[RecordPath, RecordPath],
changed: Set[RecordPath],
generated: List[str],
lib_dir: str,
) -> List[InstalledCSVRow]:
"""
:param installed: A map from archive RECORD path to installation RECORD
path.
"""
installed_rows = [] # type: List[InstalledCSVRow]
installed_rows: List[InstalledCSVRow] = []
for row in old_csv_rows:
if len(row) > 3:
logger.warning('RECORD line has more than three elements: %s', row)
old_record_path = _parse_record_path(row[0])
logger.warning("RECORD line has more than three elements: %s", row)
old_record_path = cast("RecordPath", row[0])
new_record_path = installed.pop(old_record_path, old_record_path)
if new_record_path in changed:
digest, length = rehash(_record_to_fs_path(new_record_path))
else:
digest = row[1] if len(row) > 1 else ''
length = row[2] if len(row) > 2 else ''
digest = row[1] if len(row) > 1 else ""
length = row[2] if len(row) > 2 else ""
installed_rows.append((new_record_path, digest, length))
for f in generated:
path = _fs_to_record_path(f, lib_dir)
digest, length = rehash(f)
installed_rows.append((path, digest, length))
for installed_record_path in installed.values():
installed_rows.append((installed_record_path, '', ''))
installed_rows.append((installed_record_path, "", ""))
return installed_rows
def get_console_script_specs(console):
# type: (Dict[str, str]) -> List[str]
def get_console_script_specs(console: Dict[str, str]) -> List[str]:
"""
Given the mapping from entrypoint name to callable, return the relevant
console script specs.
@@ -326,62 +315,57 @@ def get_console_script_specs(console):
# DEFAULT
# - The default behavior is to install pip, pipX, pipX.Y, easy_install
# and easy_install-X.Y.
pip_script = console.pop('pip', None)
pip_script = console.pop("pip", None)
if pip_script:
if "ENSUREPIP_OPTIONS" not in os.environ:
scripts_to_generate.append('pip = ' + pip_script)
scripts_to_generate.append("pip = " + pip_script)
if os.environ.get("ENSUREPIP_OPTIONS", "") != "altinstall":
scripts_to_generate.append(
'pip{} = {}'.format(sys.version_info[0], pip_script)
"pip{} = {}".format(sys.version_info[0], pip_script)
)
scripts_to_generate.append(
f'pip{get_major_minor_version()} = {pip_script}'
)
scripts_to_generate.append(f"pip{get_major_minor_version()} = {pip_script}")
# Delete any other versioned pip entry points
pip_ep = [k for k in console if re.match(r'pip(\d(\.\d)?)?$', k)]
pip_ep = [k for k in console if re.match(r"pip(\d(\.\d)?)?$", k)]
for k in pip_ep:
del console[k]
easy_install_script = console.pop('easy_install', None)
easy_install_script = console.pop("easy_install", None)
if easy_install_script:
if "ENSUREPIP_OPTIONS" not in os.environ:
scripts_to_generate.append(
'easy_install = ' + easy_install_script
)
scripts_to_generate.append("easy_install = " + easy_install_script)
scripts_to_generate.append(
'easy_install-{} = {}'.format(
"easy_install-{} = {}".format(
get_major_minor_version(), easy_install_script
)
)
# Delete any other versioned easy_install entry points
easy_install_ep = [
k for k in console if re.match(r'easy_install(-\d\.\d)?$', k)
k for k in console if re.match(r"easy_install(-\d\.\d)?$", k)
]
for k in easy_install_ep:
del console[k]
# Generate the console entry points specified in the wheel
scripts_to_generate.extend(starmap('{} = {}'.format, console.items()))
scripts_to_generate.extend(starmap("{} = {}".format, console.items()))
return scripts_to_generate
class ZipBackedFile:
def __init__(self, src_record_path, dest_path, zip_file):
# type: (RecordPath, str, ZipFile) -> None
def __init__(
self, src_record_path: RecordPath, dest_path: str, zip_file: ZipFile
) -> None:
self.src_record_path = src_record_path
self.dest_path = dest_path
self._zip_file = zip_file
self.changed = False
def _getinfo(self):
# type: () -> ZipInfo
def _getinfo(self) -> ZipInfo:
return self._zip_file.getinfo(self.src_record_path)
def save(self):
# type: () -> None
def save(self) -> None:
# directory creation is lazy and after file filtering
# to ensure we don't install empty dirs; empty dirs can't be
# uninstalled.
@@ -410,22 +394,19 @@ class ZipBackedFile:
class ScriptFile:
def __init__(self, file):
# type: (File) -> None
def __init__(self, file: "File") -> None:
self._file = file
self.src_record_path = self._file.src_record_path
self.dest_path = self._file.dest_path
self.changed = False
def save(self):
# type: () -> None
def save(self) -> None:
self._file.save()
self.changed = fix_script(self.dest_path)
class MissingCallableSuffix(InstallationError):
def __init__(self, entry_point):
# type: (str) -> None
def __init__(self, entry_point: str) -> None:
super().__init__(
"Invalid script entry point: {} - A callable "
"suffix is required. Cf https://packaging.python.org/"
@@ -434,31 +415,28 @@ class MissingCallableSuffix(InstallationError):
)
def _raise_for_invalid_entrypoint(specification):
# type: (str) -> None
def _raise_for_invalid_entrypoint(specification: str) -> None:
entry = get_export_entry(specification)
if entry is not None and entry.suffix is None:
raise MissingCallableSuffix(str(entry))
class PipScriptMaker(ScriptMaker):
def make(self, specification, options=None):
# type: (str, Dict[str, Any]) -> List[str]
def make(self, specification: str, options: Dict[str, Any] = None) -> List[str]:
_raise_for_invalid_entrypoint(specification)
return super().make(specification, options)
def _install_wheel(
name, # type: str
wheel_zip, # type: ZipFile
wheel_path, # type: str
scheme, # type: Scheme
pycompile=True, # type: bool
warn_script_location=True, # type: bool
direct_url=None, # type: Optional[DirectUrl]
requested=False, # type: bool
):
# type: (...) -> None
name: str,
wheel_zip: ZipFile,
wheel_path: str,
scheme: Scheme,
pycompile: bool = True,
warn_script_location: bool = True,
direct_url: Optional[DirectUrl] = None,
requested: bool = False,
) -> None:
"""Install a wheel.
:param name: Name of the project to install
@@ -485,33 +463,23 @@ def _install_wheel(
# installed = files copied from the wheel to the destination
# changed = files changed while installing (scripts #! line typically)
# generated = files newly generated during the install (script wrappers)
installed = {} # type: Dict[RecordPath, RecordPath]
changed = set() # type: Set[RecordPath]
generated = [] # type: List[str]
installed: Dict[RecordPath, RecordPath] = {}
changed: Set[RecordPath] = set()
generated: List[str] = []
def record_installed(srcfile, destfile, modified=False):
# type: (RecordPath, str, bool) -> None
def record_installed(
srcfile: RecordPath, destfile: str, modified: bool = False
) -> None:
"""Map archive RECORD paths to installation RECORD paths."""
newpath = _fs_to_record_path(destfile, lib_dir)
installed[srcfile] = newpath
if modified:
changed.add(_fs_to_record_path(destfile))
def all_paths():
# type: () -> Iterable[RecordPath]
names = wheel_zip.namelist()
# If a flag is set, names may be unicode in Python 2. We convert to
# text explicitly so these are valid for lookup in RECORD.
decoded_names = map(ensure_text, names)
for name in decoded_names:
yield cast("RecordPath", name)
def is_dir_path(path):
# type: (RecordPath) -> bool
def is_dir_path(path: RecordPath) -> bool:
return path.endswith("/")
def assert_no_path_traversal(dest_dir_path, target_path):
# type: (str, str) -> None
def assert_no_path_traversal(dest_dir_path: str, target_path: str) -> None:
if not is_within_directory(dest_dir_path, target_path):
message = (
"The wheel {!r} has a file {!r} trying to install"
@@ -521,10 +489,10 @@ def _install_wheel(
message.format(wheel_path, target_path, dest_dir_path)
)
def root_scheme_file_maker(zip_file, dest):
# type: (ZipFile, str) -> Callable[[RecordPath], File]
def make_root_scheme_file(record_path):
# type: (RecordPath) -> File
def root_scheme_file_maker(
zip_file: ZipFile, dest: str
) -> Callable[[RecordPath], "File"]:
def make_root_scheme_file(record_path: RecordPath) -> "File":
normed_path = os.path.normpath(record_path)
dest_path = os.path.join(dest, normed_path)
assert_no_path_traversal(dest, dest_path)
@@ -532,17 +500,12 @@ def _install_wheel(
return make_root_scheme_file
def data_scheme_file_maker(zip_file, scheme):
# type: (ZipFile, Scheme) -> Callable[[RecordPath], File]
scheme_paths = {}
for key in SCHEME_KEYS:
encoded_key = ensure_text(key)
scheme_paths[encoded_key] = ensure_text(
getattr(scheme, key), encoding=sys.getfilesystemencoding()
)
def data_scheme_file_maker(
zip_file: ZipFile, scheme: Scheme
) -> Callable[[RecordPath], "File"]:
scheme_paths = {key: getattr(scheme, key) for key in SCHEME_KEYS}
def make_data_scheme_file(record_path):
# type: (RecordPath) -> File
def make_data_scheme_file(record_path: RecordPath) -> "File":
normed_path = os.path.normpath(record_path)
try:
_, scheme_key, dest_subpath = normed_path.split(os.path.sep, 2)
@@ -561,9 +524,7 @@ def _install_wheel(
"Unknown scheme key used in {}: {} (for file {!r}). .data"
" directory contents should be in subdirectories named"
" with a valid scheme key ({})"
).format(
wheel_path, scheme_key, record_path, valid_scheme_keys
)
).format(wheel_path, scheme_key, record_path, valid_scheme_keys)
raise InstallationError(message)
dest_path = os.path.join(scheme_path, dest_subpath)
@@ -572,30 +533,19 @@ def _install_wheel(
return make_data_scheme_file
def is_data_scheme_path(path):
# type: (RecordPath) -> bool
def is_data_scheme_path(path: RecordPath) -> bool:
return path.split("/", 1)[0].endswith(".data")
paths = all_paths()
paths = cast(List[RecordPath], wheel_zip.namelist())
file_paths = filterfalse(is_dir_path, paths)
root_scheme_paths, data_scheme_paths = partition(
is_data_scheme_path, file_paths
)
root_scheme_paths, data_scheme_paths = partition(is_data_scheme_path, file_paths)
make_root_scheme_file = root_scheme_file_maker(
wheel_zip,
ensure_text(lib_dir, encoding=sys.getfilesystemencoding()),
)
files = map(make_root_scheme_file, root_scheme_paths)
make_root_scheme_file = root_scheme_file_maker(wheel_zip, lib_dir)
files: Iterator[File] = map(make_root_scheme_file, root_scheme_paths)
def is_script_scheme_path(path):
# type: (RecordPath) -> bool
def is_script_scheme_path(path: RecordPath) -> bool:
parts = path.split("/", 2)
return (
len(parts) > 2 and
parts[0].endswith(".data") and
parts[1] == "scripts"
)
return len(parts) > 2 and parts[0].endswith(".data") and parts[1] == "scripts"
other_scheme_paths, script_scheme_paths = partition(
is_script_scheme_path, data_scheme_paths
@@ -606,30 +556,32 @@ def _install_wheel(
files = chain(files, other_scheme_files)
# Get the defined entry points
distribution = get_wheel_distribution(wheel_path, canonicalize_name(name))
distribution = get_wheel_distribution(
FilesystemWheel(wheel_path),
canonicalize_name(name),
)
console, gui = get_entrypoints(distribution)
def is_entrypoint_wrapper(file):
# type: (File) -> bool
def is_entrypoint_wrapper(file: "File") -> bool:
# EP, EP.exe and EP-script.py are scripts generated for
# entry point EP by setuptools
path = file.dest_path
name = os.path.basename(path)
if name.lower().endswith('.exe'):
if name.lower().endswith(".exe"):
matchname = name[:-4]
elif name.lower().endswith('-script.py'):
elif name.lower().endswith("-script.py"):
matchname = name[:-10]
elif name.lower().endswith(".pya"):
matchname = name[:-4]
else:
matchname = name
# Ignore setuptools-generated scripts
return (matchname in console or matchname in gui)
return matchname in console or matchname in gui
script_scheme_files = map(make_data_scheme_file, script_scheme_paths)
script_scheme_files = filterfalse(
is_entrypoint_wrapper, script_scheme_files
script_scheme_files: Iterator[File] = map(
make_data_scheme_file, script_scheme_paths
)
script_scheme_files = filterfalse(is_entrypoint_wrapper, script_scheme_files)
script_scheme_files = map(ScriptFile, script_scheme_files)
files = chain(files, script_scheme_files)
@@ -637,8 +589,7 @@ def _install_wheel(
file.save()
record_installed(file.src_record_path, file.dest_path, file.changed)
def pyc_source_file_paths():
# type: () -> Iterator[str]
def pyc_source_file_paths() -> Iterator[str]:
# We de-duplicate installation paths, since there can be overlap (e.g.
# file in .data maps to same location as file in wheel root).
# Sorting installation paths makes it easier to reproduce and debug
@@ -647,30 +598,21 @@ def _install_wheel(
full_installed_path = os.path.join(lib_dir, installed_path)
if not os.path.isfile(full_installed_path):
continue
if not full_installed_path.endswith('.py'):
if not full_installed_path.endswith(".py"):
continue
yield full_installed_path
def pyc_output_path(path):
# type: (str) -> str
"""Return the path the pyc file would have been written to.
"""
def pyc_output_path(path: str) -> str:
"""Return the path the pyc file would have been written to."""
return importlib.util.cache_from_source(path)
# Compile all of the pyc files for the installed files
if pycompile:
with captured_stdout() as stdout:
with warnings.catch_warnings():
warnings.filterwarnings('ignore')
warnings.filterwarnings("ignore")
for path in pyc_source_file_paths():
# Python 2's `compileall.compile_file` requires a str in
# error cases, so we must convert to the native type.
path_arg = ensure_str(
path, encoding=sys.getfilesystemencoding()
)
success = compileall.compile_file(
path_arg, force=True, quiet=True
)
success = compileall.compile_file(path, force=True, quiet=True)
if success:
pyc_path = pyc_output_path(path)
assert os.path.exists(pyc_path)
@@ -689,7 +631,7 @@ def _install_wheel(
# Ensure we don't generate any variants for scripts because this is almost
# never what somebody wants.
# See https://bitbucket.org/pypa/distlib/issue/35/
maker.variants = {''}
maker.variants = {""}
# This is required because otherwise distlib creates scripts that are not
# executable.
@@ -699,14 +641,12 @@ def _install_wheel(
# Generate the console and GUI entry points specified in the wheel
scripts_to_generate = get_console_script_specs(console)
gui_scripts_to_generate = list(starmap('{} = {}'.format, gui.items()))
gui_scripts_to_generate = list(starmap("{} = {}".format, gui.items()))
generated_console_scripts = maker.make_multiple(scripts_to_generate)
generated.extend(generated_console_scripts)
generated.extend(
maker.make_multiple(gui_scripts_to_generate, {'gui': True})
)
generated.extend(maker.make_multiple(gui_scripts_to_generate, {"gui": True}))
if warn_script_location:
msg = message_about_scripts_not_on_PATH(generated_console_scripts)
@@ -716,8 +656,7 @@ def _install_wheel(
generated_file_mode = 0o666 & ~current_umask()
@contextlib.contextmanager
def _generate_file(path, **kwargs):
# type: (str, **Any) -> Iterator[BinaryIO]
def _generate_file(path: str, **kwargs: Any) -> Iterator[BinaryIO]:
with adjacent_tmp_file(path, **kwargs) as f:
yield f
os.chmod(f.name, generated_file_mode)
@@ -726,9 +665,9 @@ def _install_wheel(
dest_info_dir = os.path.join(lib_dir, info_dir)
# Record pip as the installer
installer_path = os.path.join(dest_info_dir, 'INSTALLER')
installer_path = os.path.join(dest_info_dir, "INSTALLER")
with _generate_file(installer_path) as installer_file:
installer_file.write(b'pip\n')
installer_file.write(b"pip\n")
generated.append(installer_path)
# Record the PEP 610 direct URL reference
@@ -740,12 +679,12 @@ def _install_wheel(
# Record the REQUESTED file
if requested:
requested_path = os.path.join(dest_info_dir, 'REQUESTED')
requested_path = os.path.join(dest_info_dir, "REQUESTED")
with open(requested_path, "wb"):
pass
generated.append(requested_path)
record_text = distribution.read_text('RECORD')
record_text = distribution.read_text("RECORD")
record_rows = list(csv.reader(record_text.splitlines()))
rows = get_csv_rows_for_installed(
@@ -753,42 +692,38 @@ def _install_wheel(
installed=installed,
changed=changed,
generated=generated,
lib_dir=lib_dir)
lib_dir=lib_dir,
)
# Record details of all files installed
record_path = os.path.join(dest_info_dir, 'RECORD')
record_path = os.path.join(dest_info_dir, "RECORD")
with _generate_file(record_path, **csv_io_kwargs('w')) as record_file:
# The type mypy infers for record_file is different for Python 3
# (typing.IO[Any]) and Python 2 (typing.BinaryIO). We explicitly
# cast to typing.IO[str] as a workaround.
writer = csv.writer(cast('IO[str]', record_file))
with _generate_file(record_path, **csv_io_kwargs("w")) as record_file:
# Explicitly cast to typing.IO[str] as a workaround for the mypy error:
# "writer" has incompatible type "BinaryIO"; expected "_Writer"
writer = csv.writer(cast("IO[str]", record_file))
writer.writerows(_normalized_outrows(rows))
@contextlib.contextmanager
def req_error_context(req_description):
# type: (str) -> Iterator[None]
def req_error_context(req_description: str) -> Iterator[None]:
try:
yield
except InstallationError as e:
message = "For req: {}. {}".format(req_description, e.args[0])
reraise(
InstallationError, InstallationError(message), sys.exc_info()[2]
)
raise InstallationError(message) from e
def install_wheel(
name, # type: str
wheel_path, # type: str
scheme, # type: Scheme
req_description, # type: str
pycompile=True, # type: bool
warn_script_location=True, # type: bool
direct_url=None, # type: Optional[DirectUrl]
requested=False, # type: bool
):
# type: (...) -> None
name: str,
wheel_path: str,
scheme: Scheme,
req_description: str,
pycompile: bool = True,
warn_script_location: bool = True,
direct_url: Optional[DirectUrl] = None,
requested: bool = False,
) -> None:
with ZipFile(wheel_path, allowZip64=True) as z:
with req_error_context(req_description):
_install_wheel(
@@ -8,10 +8,9 @@ import logging
import mimetypes
import os
import shutil
from typing import Dict, Iterable, List, Optional, Tuple
from typing import Dict, Iterable, List, Optional
from pipenv.patched.notpip._vendor.packaging.utils import canonicalize_name
from pipenv.patched.notpip._vendor.pkg_resources import Distribution
from pipenv.patched.notpip._internal.distributions import make_distribution_for_install_requirement
from pipenv.patched.notpip._internal.distributions.installed import InstalledDistribution
@@ -25,6 +24,7 @@ from pipenv.patched.notpip._internal.exceptions import (
VcsHashUnsupported,
)
from pipenv.patched.notpip._internal.index.package_finder import PackageFinder
from pipenv.patched.notpip._internal.metadata import BaseDistribution
from pipenv.patched.notpip._internal.models.link import Link
from pipenv.patched.notpip._internal.models.wheel import Wheel
from pipenv.patched.notpip._internal.network.download import BatchDownloader, Downloader
@@ -35,7 +35,6 @@ from pipenv.patched.notpip._internal.network.lazy_wheel import (
from pipenv.patched.notpip._internal.network.session import PipSession
from pipenv.patched.notpip._internal.req.req_install import InstallRequirement
from pipenv.patched.notpip._internal.req.req_tracker import RequirementTracker
from pipenv.patched.notpip._internal.utils.deprecation import deprecated
from pipenv.patched.notpip._internal.utils.filesystem import copy2_fixed
from pipenv.patched.notpip._internal.utils.hashes import Hashes, MissingHashes
from pipenv.patched.notpip._internal.utils.logging import indent_log
@@ -48,30 +47,26 @@ logger = logging.getLogger(__name__)
def _get_prepared_distribution(
req, # type: InstallRequirement
req_tracker, # type: RequirementTracker
finder, # type: PackageFinder
build_isolation, # type: bool
):
# type: (...) -> Distribution
req: InstallRequirement,
req_tracker: RequirementTracker,
finder: PackageFinder,
build_isolation: bool,
) -> BaseDistribution:
"""Prepare a distribution for installation."""
abstract_dist = make_distribution_for_install_requirement(req)
with req_tracker.track(req):
abstract_dist.prepare_distribution_metadata(finder, build_isolation)
return abstract_dist.get_pkg_resources_distribution()
return abstract_dist.get_metadata_distribution()
def unpack_vcs_link(link, location):
# type: (Link, str) -> None
def unpack_vcs_link(link: Link, location: str, verbosity: int) -> None:
vcs_backend = vcs.get_backend_for_scheme(link.scheme)
assert vcs_backend is not None
vcs_backend.unpack(location, url=hide_url(link.url))
vcs_backend.unpack(location, url=hide_url(link.url), verbosity=verbosity)
class File:
def __init__(self, path, content_type):
# type: (str, Optional[str]) -> None
def __init__(self, path: str, content_type: Optional[str]) -> None:
self.path = path
if content_type is None:
self.content_type = mimetypes.guess_type(path)[0]
@@ -80,19 +75,16 @@ class File:
def get_http_url(
link, # type: Link
download, # type: Downloader
download_dir=None, # type: Optional[str]
hashes=None, # type: Optional[Hashes]
):
# type: (...) -> File
link: Link,
download: Downloader,
download_dir: Optional[str] = None,
hashes: Optional[Hashes] = None,
) -> File:
temp_dir = TempDirectory(kind="unpack", globally_managed=True)
# If a download dir is specified, is the file already downloaded there?
already_downloaded_path = None
if download_dir:
already_downloaded_path = _check_download_dir(
link, download_dir, hashes
)
already_downloaded_path = _check_download_dir(link, download_dir, hashes)
if already_downloaded_path:
from_path = already_downloaded_path
@@ -106,8 +98,7 @@ def get_http_url(
return File(from_path, content_type)
def _copy2_ignoring_special_files(src, dest):
# type: (str, str) -> None
def _copy2_ignoring_special_files(src: str, dest: str) -> None:
"""Copying special files is not supported, but as a convenience to users
we skip errors copying them. This supports tools that may create e.g.
socket files in the project source directory.
@@ -127,21 +118,19 @@ def _copy2_ignoring_special_files(src, dest):
)
def _copy_source_tree(source, target):
# type: (str, str) -> None
def _copy_source_tree(source: str, target: str) -> None:
target_abspath = os.path.abspath(target)
target_basename = os.path.basename(target_abspath)
target_dirname = os.path.dirname(target_abspath)
def ignore(d, names):
# type: (str, List[str]) -> List[str]
skipped = [] # type: List[str]
def ignore(d: str, names: List[str]) -> List[str]:
skipped: List[str] = []
if d == source:
# Pulling in those directories can potentially be very slow,
# exclude the following directories if they appear in the top
# level dir (and only it).
# See discussion at https://github.com/pypa/pip/pull/6770
skipped += ['.tox', '.nox']
skipped += [".tox", ".nox"]
if os.path.abspath(d) == target_dirname:
# Prevent an infinite recursion if the target is in source.
# This can happen when TMPDIR is set to ${PWD}/...
@@ -159,19 +148,13 @@ def _copy_source_tree(source, target):
def get_file_url(
link, # type: Link
download_dir=None, # type: Optional[str]
hashes=None # type: Optional[Hashes]
):
# type: (...) -> File
"""Get file and optionally check its hash.
"""
link: Link, download_dir: Optional[str] = None, hashes: Optional[Hashes] = None
) -> File:
"""Get file and optionally check its hash."""
# If a download dir is specified, is the file already there and valid?
already_downloaded_path = None
if download_dir:
already_downloaded_path = _check_download_dir(
link, download_dir, hashes
)
already_downloaded_path = _check_download_dir(link, download_dir, hashes)
if already_downloaded_path:
from_path = already_downloaded_path
@@ -189,13 +172,13 @@ def get_file_url(
def unpack_url(
link, # type: Link
location, # type: str
download, # type: Downloader
download_dir=None, # type: Optional[str]
hashes=None, # type: Optional[Hashes]
):
# type: (...) -> Optional[File]
link: Link,
location: str,
download: Downloader,
verbosity: int,
download_dir: Optional[str] = None,
hashes: Optional[Hashes] = None,
) -> Optional[File]:
"""Unpack link into location, downloading if required.
:param hashes: A Hashes object, one of whose embedded hashes must match,
@@ -205,7 +188,7 @@ def unpack_url(
"""
# non-editable vcs urls
if link.is_vcs:
unpack_vcs_link(link, location)
unpack_vcs_link(link, location, verbosity=verbosity)
return None
# Once out-of-tree-builds are no longer supported, could potentially
@@ -214,17 +197,9 @@ def unpack_url(
#
# As further cleanup, _copy_source_tree and accompanying tests can
# be removed.
#
# TODO when use-deprecated=out-of-tree-build is removed
if link.is_existing_dir():
deprecated(
"A future pip version will change local packages to be built "
"in-place without first copying to a temporary directory. "
"We recommend you use --use-feature=in-tree-build to test "
"your packages with this new behavior before it becomes the "
"default.\n",
replacement=None,
gone_in="21.3",
issue=7555
)
if os.path.isdir(location):
rmtree(location)
_copy_source_tree(link.file_path, location)
@@ -251,10 +226,11 @@ def unpack_url(
return file
def _check_download_dir(link, download_dir, hashes):
# type: (Link, str, Optional[Hashes]) -> Optional[str]
""" Check download_dir for previously downloaded file with correct hash
If a correct file is found return its path else None
def _check_download_dir(
link: Link, download_dir: str, hashes: Optional[Hashes]
) -> Optional[str]:
"""Check download_dir for previously downloaded file with correct hash
If a correct file is found return its path else None
"""
download_path = os.path.join(download_dir, link.filename)
@@ -262,15 +238,14 @@ def _check_download_dir(link, download_dir, hashes):
return None
# If already downloaded, does its hash match?
logger.info('File was already downloaded %s', download_path)
logger.info("File was already downloaded %s", download_path)
if hashes:
try:
hashes.check_against_path(download_path)
except HashMismatch:
logger.warning(
'Previously-downloaded file %s has bad hash. '
'Re-downloading.',
download_path
"Previously-downloaded file %s has bad hash. Re-downloading.",
download_path,
)
os.unlink(download_path)
return None
@@ -278,25 +253,24 @@ def _check_download_dir(link, download_dir, hashes):
class RequirementPreparer:
"""Prepares a Requirement
"""
"""Prepares a Requirement"""
def __init__(
self,
build_dir, # type: str
download_dir, # type: Optional[str]
src_dir, # type: str
build_isolation, # type: bool
req_tracker, # type: RequirementTracker
session, # type: PipSession
progress_bar, # type: str
finder, # type: PackageFinder
require_hashes, # type: bool
use_user_site, # type: bool
lazy_wheel, # type: bool
in_tree_build, # type: bool
):
# type: (...) -> None
build_dir: str,
download_dir: Optional[str],
src_dir: str,
build_isolation: bool,
req_tracker: RequirementTracker,
session: PipSession,
progress_bar: str,
finder: PackageFinder,
require_hashes: bool,
use_user_site: bool,
lazy_wheel: bool,
verbosity: int,
in_tree_build: bool,
) -> None:
super().__init__()
self.src_dir = src_dir
@@ -323,17 +297,19 @@ class RequirementPreparer:
# Should wheels be downloaded lazily?
self.use_lazy_wheel = lazy_wheel
# How verbose should underlying tooling be?
self.verbosity = verbosity
# Should in-tree builds be used for local paths?
self.in_tree_build = in_tree_build
# Memoized downloaded files, as mapping of url: (path, mime type)
self._downloaded = {} # type: Dict[str, Tuple[str, str]]
# Memoized downloaded files, as mapping of url: path.
self._downloaded: Dict[str, str] = {}
# Previous "header" printed for a link-based InstallRequirement
self._previous_requirement_header = ("", "")
def _log_preparing_link(self, req):
# type: (InstallRequirement) -> None
def _log_preparing_link(self, req: InstallRequirement) -> None:
"""Provide context for the requirement being prepared."""
if req.link.is_file and not req.original_link_is_in_wheel_cache:
message = "Processing %s"
@@ -350,8 +326,9 @@ class RequirementPreparer:
with indent_log():
logger.info("Using cached %s", req.link.filename)
def _ensure_link_req_src_dir(self, req, parallel_builds):
# type: (InstallRequirement, bool) -> None
def _ensure_link_req_src_dir(
self, req: InstallRequirement, parallel_builds: bool
) -> None:
"""Ensure source_dir of a linked InstallRequirement."""
# Since source_dir is only set for editable requirements.
if req.link.is_wheel:
@@ -376,6 +353,7 @@ class RequirementPreparer:
# installation.
# FIXME: this won't upgrade when there's an existing
# package unpacked in `req.source_dir`
# TODO: this check is now probably dead code
if is_installable_dir(req.source_dir):
raise PreviousBuildDirError(
"pip can't proceed with requirements '{}' due to a"
@@ -385,8 +363,7 @@ class RequirementPreparer:
"Please delete it and try again.".format(req, req.source_dir)
)
def _get_linked_req_hashes(self, req):
# type: (InstallRequirement) -> Hashes
def _get_linked_req_hashes(self, req: InstallRequirement) -> Hashes:
# By the time this is called, the requirement's link should have
# been checked so we can tell what kind of requirements req is
# and raise some more informative errors than otherwise.
@@ -418,18 +395,19 @@ class RequirementPreparer:
# showing the user what the hash should be.
return req.hashes(trust_internet=False) or MissingHashes()
def _fetch_metadata_using_lazy_wheel(self, link):
# type: (Link) -> Optional[Distribution]
def _fetch_metadata_using_lazy_wheel(
self,
link: Link,
) -> Optional[BaseDistribution]:
"""Fetch metadata using lazy wheel, if possible."""
if not self.use_lazy_wheel:
return None
if self.require_hashes:
logger.debug('Lazy wheel is not used as hash checking is required')
logger.debug("Lazy wheel is not used as hash checking is required")
return None
if link.is_file or not link.is_wheel:
logger.debug(
'Lazy wheel is not used as '
'%r does not points to a remote wheel',
"Lazy wheel is not used as %r does not points to a remote wheel",
link,
)
return None
@@ -437,22 +415,22 @@ class RequirementPreparer:
wheel = Wheel(link.filename)
name = canonicalize_name(wheel.name)
logger.info(
'Obtaining dependency information from %s %s',
name, wheel.version,
"Obtaining dependency information from %s %s",
name,
wheel.version,
)
url = link.url.split('#', 1)[0]
url = link.url.split("#", 1)[0]
try:
return dist_from_wheel_url(name, url, self._session)
except HTTPRangeRequestUnsupported:
logger.debug('%s does not support range requests', url)
logger.debug("%s does not support range requests", url)
return None
def _complete_partial_requirements(
self,
partially_downloaded_reqs, # type: Iterable[InstallRequirement]
parallel_builds=False, # type: bool
):
# type: (...) -> None
partially_downloaded_reqs: Iterable[InstallRequirement],
parallel_builds: bool = False,
) -> None:
"""Download any requirements which were only fetched by metadata."""
# Download to a temporary directory. These will be copied over as
# needed for downstream 'download', 'wheel', and 'install' commands.
@@ -461,7 +439,7 @@ class RequirementPreparer:
# Map each link to the requirement that owns it. This allows us to set
# `req.local_file_path` on the appropriate requirement after passing
# all the links at once into BatchDownloader.
links_to_fully_download = {} # type: Dict[Link, InstallRequirement]
links_to_fully_download: Dict[Link, InstallRequirement] = {}
for req in partially_downloaded_reqs:
assert req.link
links_to_fully_download[req.link] = req
@@ -480,8 +458,9 @@ class RequirementPreparer:
for req in partially_downloaded_reqs:
self._prepare_linked_requirement(req, parallel_builds)
def prepare_linked_requirement(self, req, parallel_builds=False):
# type: (InstallRequirement, bool) -> Distribution
def prepare_linked_requirement(
self, req: InstallRequirement, parallel_builds: bool = False
) -> BaseDistribution:
"""Prepare a requirement to be obtained from req.link."""
assert req.link
link = req.link
@@ -496,7 +475,7 @@ class RequirementPreparer:
if file_path is not None:
# The file is already available, so mark it as downloaded
self._downloaded[req.link.url] = file_path, None
self._downloaded[req.link.url] = file_path
else:
# The file is not available, attempt to fetch only metadata
wheel_dist = self._fetch_metadata_using_lazy_wheel(link)
@@ -507,8 +486,9 @@ class RequirementPreparer:
# None of the optimizations worked, fully prepare the requirement
return self._prepare_linked_requirement(req, parallel_builds)
def prepare_linked_requirements_more(self, reqs, parallel_builds=False):
# type: (Iterable[InstallRequirement], bool) -> None
def prepare_linked_requirements_more(
self, reqs: Iterable[InstallRequirement], parallel_builds: bool = False
) -> None:
"""Prepare linked requirements more, if needed."""
reqs = [req for req in reqs if req.needs_more_preparation]
for req in reqs:
@@ -517,12 +497,12 @@ class RequirementPreparer:
hashes = self._get_linked_req_hashes(req)
file_path = _check_download_dir(req.link, self.download_dir, hashes)
if file_path is not None:
self._downloaded[req.link.url] = file_path, None
self._downloaded[req.link.url] = file_path
req.needs_more_preparation = False
# Prepare requirements we found were already downloaded for some
# reason. The other downloads will be completed separately.
partially_downloaded_reqs = [] # type: List[InstallRequirement]
partially_downloaded_reqs: List[InstallRequirement] = []
for req in reqs:
if req.needs_more_preparation:
partially_downloaded_reqs.append(req)
@@ -532,11 +512,13 @@ class RequirementPreparer:
# TODO: separate this part out from RequirementPreparer when the v1
# resolver can be removed!
self._complete_partial_requirements(
partially_downloaded_reqs, parallel_builds=parallel_builds,
partially_downloaded_reqs,
parallel_builds=parallel_builds,
)
def _prepare_linked_requirement(self, req, parallel_builds):
# type: (InstallRequirement, bool) -> Distribution
def _prepare_linked_requirement(
self, req: InstallRequirement, parallel_builds: bool
) -> BaseDistribution:
assert req.link
link = req.link
@@ -548,19 +530,23 @@ class RequirementPreparer:
elif link.url not in self._downloaded:
try:
local_file = unpack_url(
link, req.source_dir, self._download,
self.download_dir, hashes
link,
req.source_dir,
self._download,
self.verbosity,
self.download_dir,
hashes,
)
except NetworkConnectionError as exc:
raise InstallationError(
'Could not install requirement {} because of HTTP '
'error {} for URL {}'.format(req, exc, link)
"Could not install requirement {} because of HTTP "
"error {} for URL {}".format(req, exc, link)
)
else:
file_path, content_type = self._downloaded[link.url]
file_path = self._downloaded[link.url]
if hashes:
hashes.check_against_path(file_path)
local_file = File(file_path, content_type)
local_file = File(file_path, content_type=None)
# For use in later processing,
# preserve the file path on the requirement.
@@ -568,12 +554,14 @@ class RequirementPreparer:
req.local_file_path = local_file.path
dist = _get_prepared_distribution(
req, self.req_tracker, self.finder, self.build_isolation,
req,
self.req_tracker,
self.finder,
self.build_isolation,
)
return dist
def save_linked_requirement(self, req):
# type: (InstallRequirement) -> None
def save_linked_requirement(self, req: InstallRequirement) -> None:
assert self.download_dir is not None
assert req.link is not None
link = req.link
@@ -584,8 +572,9 @@ class RequirementPreparer:
if link.is_existing_dir():
logger.debug(
'Not copying link to destination directory '
'since it is a directory: %s', link,
"Not copying link to destination directory "
"since it is a directory: %s",
link,
)
return
if req.local_file_path is None:
@@ -596,31 +585,32 @@ class RequirementPreparer:
if not os.path.exists(download_location):
shutil.copy(req.local_file_path, download_location)
download_path = display_path(download_location)
logger.info('Saved %s', download_path)
logger.info("Saved %s", download_path)
def prepare_editable_requirement(
self,
req, # type: InstallRequirement
):
# type: (...) -> Distribution
"""Prepare an editable requirement
"""
req: InstallRequirement,
) -> BaseDistribution:
"""Prepare an editable requirement."""
assert req.editable, "cannot prepare a non-editable req as editable"
logger.info('Obtaining %s', req)
logger.info("Obtaining %s", req)
with indent_log():
if self.require_hashes:
raise InstallationError(
'The editable requirement {} cannot be installed when '
'requiring hashes, because there is no single file to '
'hash.'.format(req)
"The editable requirement {} cannot be installed when "
"requiring hashes, because there is no single file to "
"hash.".format(req)
)
req.ensure_has_source_dir(self.src_dir)
req.update_editable()
dist = _get_prepared_distribution(
req, self.req_tracker, self.finder, self.build_isolation,
req,
self.req_tracker,
self.finder,
self.build_isolation,
)
req.check_if_exists(self.use_user_site)
@@ -629,27 +619,24 @@ class RequirementPreparer:
def prepare_installed_requirement(
self,
req, # type: InstallRequirement
skip_reason # type: str
):
# type: (...) -> Distribution
"""Prepare an already-installed requirement
"""
req: InstallRequirement,
skip_reason: str,
) -> BaseDistribution:
"""Prepare an already-installed requirement."""
assert req.satisfied_by, "req should have been satisfied but isn't"
assert skip_reason is not None, (
"did not get skip reason skipped but req.satisfied_by "
"is set to {}".format(req.satisfied_by)
)
logger.info(
'Requirement %s: %s (%s)',
skip_reason, req, req.satisfied_by.version
"Requirement %s: %s (%s)", skip_reason, req, req.satisfied_by.version
)
with indent_log():
if self.require_hashes:
logger.debug(
'Since it is already installed, we are trusting this '
'package without checking its hash. To ensure a '
'completely repeatable environment, install into an '
'empty virtualenv.'
"Since it is already installed, we are trusting this "
"package without checking its hash. To ensure a "
"completely repeatable environment, install into an "
"empty virtualenv."
)
return InstalledDistribution(req).get_pkg_resources_distribution()
return InstalledDistribution(req).get_metadata_distribution()
+32 -47
View File
@@ -5,34 +5,29 @@ from typing import Any, List, Optional
from pipenv.patched.notpip._vendor import tomli
from pipenv.patched.notpip._vendor.packaging.requirements import InvalidRequirement, Requirement
from pipenv.patched.notpip._internal.exceptions import InstallationError
from pipenv.patched.notpip._internal.exceptions import (
InstallationError,
InvalidPyProjectBuildRequires,
MissingPyProjectBuildRequires,
)
def _is_list_of_str(obj):
# type: (Any) -> bool
return (
isinstance(obj, list) and
all(isinstance(item, str) for item in obj)
)
def _is_list_of_str(obj: Any) -> bool:
return isinstance(obj, list) and all(isinstance(item, str) for item in obj)
def make_pyproject_path(unpacked_source_directory):
# type: (str) -> str
return os.path.join(unpacked_source_directory, 'pyproject.toml')
def make_pyproject_path(unpacked_source_directory: str) -> str:
return os.path.join(unpacked_source_directory, "pyproject.toml")
BuildSystemDetails = namedtuple('BuildSystemDetails', [
'requires', 'backend', 'check', 'backend_path'
])
BuildSystemDetails = namedtuple(
"BuildSystemDetails", ["requires", "backend", "check", "backend_path"]
)
def load_pyproject_toml(
use_pep517, # type: Optional[bool]
pyproject_toml, # type: str
setup_py, # type: str
req_name # type: str
):
# type: (...) -> Optional[BuildSystemDetails]
use_pep517: Optional[bool], pyproject_toml: str, setup_py: str, req_name: str
) -> Optional[BuildSystemDetails]:
"""Load the pyproject.toml file.
Parameters:
@@ -57,9 +52,15 @@ def load_pyproject_toml(
has_pyproject = os.path.isfile(pyproject_toml)
has_setup = os.path.isfile(setup_py)
if not has_pyproject and not has_setup:
raise InstallationError(
f"{req_name} does not appear to be a Python project: "
f"neither 'setup.py' nor 'pyproject.toml' found."
)
if has_pyproject:
with open(pyproject_toml, encoding="utf-8") as f:
pp_toml = tomli.load(f)
pp_toml = tomli.loads(f.read())
build_system = pp_toml.get("build-system")
else:
build_system = None
@@ -82,9 +83,7 @@ def load_pyproject_toml(
raise InstallationError(
"Disabling PEP 517 processing is invalid: "
"project specifies a build backend of {} "
"in pyproject.toml".format(
build_system["build-backend"]
)
"in pyproject.toml".format(build_system["build-backend"])
)
use_pep517 = True
@@ -124,46 +123,32 @@ def load_pyproject_toml(
# Ensure that the build-system section in pyproject.toml conforms
# to PEP 518.
error_template = (
"{package} has a pyproject.toml file that does not comply "
"with PEP 518: {reason}"
)
# Specifying the build-system table but not the requires key is invalid
if "requires" not in build_system:
raise InstallationError(
error_template.format(package=req_name, reason=(
"it has a 'build-system' table but not "
"'build-system.requires' which is mandatory in the table"
))
)
raise MissingPyProjectBuildRequires(package=req_name)
# Error out if requires is not a list of strings
requires = build_system["requires"]
if not _is_list_of_str(requires):
raise InstallationError(error_template.format(
raise InvalidPyProjectBuildRequires(
package=req_name,
reason="'build-system.requires' is not a list of strings.",
))
reason="It is not a list of strings.",
)
# Each requirement must be valid as per PEP 508
for requirement in requires:
try:
Requirement(requirement)
except InvalidRequirement:
raise InstallationError(
error_template.format(
package=req_name,
reason=(
"'build-system.requires' contains an invalid "
"requirement: {!r}".format(requirement)
),
)
)
except InvalidRequirement as error:
raise InvalidPyProjectBuildRequires(
package=req_name,
reason=f"It contains an invalid requirement: {requirement!r}",
) from error
backend = build_system.get("build-backend")
backend_path = build_system.get("backend-path", [])
check = [] # type: List[str]
check: List[str] = []
if backend is None:
# If the user didn't specify a backend, we assume they want to use
# the setuptools backend. But we can't be sure they have included
@@ -9,8 +9,10 @@ from .req_install import InstallRequirement
from .req_set import RequirementSet
__all__ = [
"RequirementSet", "InstallRequirement",
"parse_requirements", "install_given_reqs",
"RequirementSet",
"InstallRequirement",
"parse_requirements",
"install_given_reqs",
]
logger = logging.getLogger(__name__)
@@ -52,8 +54,8 @@ def install_given_reqs(
if to_install:
logger.info(
'Installing collected packages: %s',
', '.join(to_install.keys()),
"Installing collected packages: %s",
", ".join(to_install.keys()),
)
installed = []
@@ -61,11 +63,9 @@ def install_given_reqs(
with indent_log():
for req_name, requirement in to_install.items():
if requirement.should_reinstall:
logger.info('Attempting uninstall: %s', req_name)
logger.info("Attempting uninstall: %s", req_name)
with indent_log():
uninstalled_pathset = requirement.uninstall(
auto_confirm=True
)
uninstalled_pathset = requirement.uninstall(auto_confirm=True)
else:
uninstalled_pathset = None
@@ -16,23 +16,23 @@ from typing import Any, Dict, Optional, Set, Tuple, Union
from pipenv.patched.notpip._vendor.packaging.markers import Marker
from pipenv.patched.notpip._vendor.packaging.requirements import InvalidRequirement, Requirement
from pipenv.patched.notpip._vendor.packaging.specifiers import Specifier
from pipenv.patched.notpip._vendor.pkg_resources import RequirementParseError, parse_requirements
from pipenv.patched.notpip._internal.exceptions import InstallationError
from pipenv.patched.notpip._internal.models.index import PyPI, TestPyPI
from pipenv.patched.notpip._internal.models.link import Link
from pipenv.patched.notpip._internal.models.wheel import Wheel
from pipenv.patched.notpip._internal.pyproject import make_pyproject_path
from pipenv.patched.notpip._internal.req.req_file import ParsedRequirement
from pipenv.patched.notpip._internal.req.req_install import InstallRequirement
from pipenv.patched.notpip._internal.utils.filetypes import is_archive_file
from pipenv.patched.notpip._internal.utils.misc import is_installable_dir
from pipenv.patched.notpip._internal.utils.packaging import get_requirement
from pipenv.patched.notpip._internal.utils.urls import path_to_url
from pipenv.patched.notpip._internal.vcs import is_url, vcs
__all__ = [
"install_req_from_editable", "install_req_from_line",
"parse_editable"
"install_req_from_editable",
"install_req_from_line",
"parse_editable",
]
logger = logging.getLogger(__name__)
@@ -40,7 +40,7 @@ operators = Specifier._operators.keys()
def _strip_extras(path: str) -> Tuple[str, Optional[str]]:
m = re.match(r'^(.+)(\[[^\]]+\])$', path)
m = re.match(r"^(.+)(\[[^\]]+\])$", path)
extras = None
if m:
path_no_extras = m.group(1)
@@ -54,7 +54,7 @@ def _strip_extras(path: str) -> Tuple[str, Optional[str]]:
def convert_extras(extras: Optional[str]) -> Set[str]:
if not extras:
return set()
return Requirement("placeholder" + extras.lower()).extras
return get_requirement("placeholder" + extras.lower()).extras
def parse_editable(editable_req: str) -> Tuple[Optional[str], str, Set[str]]:
@@ -74,39 +74,23 @@ def parse_editable(editable_req: str) -> Tuple[Optional[str], str, Set[str]]:
url_no_extras, extras = _strip_extras(url)
if os.path.isdir(url_no_extras):
setup_py = os.path.join(url_no_extras, 'setup.py')
setup_cfg = os.path.join(url_no_extras, 'setup.cfg')
if not os.path.exists(setup_py) and not os.path.exists(setup_cfg):
msg = (
'File "setup.py" or "setup.cfg" not found. Directory cannot be '
'installed in editable mode: {}'
.format(os.path.abspath(url_no_extras))
)
pyproject_path = make_pyproject_path(url_no_extras)
if os.path.isfile(pyproject_path):
msg += (
'\n(A "pyproject.toml" file was found, but editable '
'mode currently requires a setuptools-based build.)'
)
raise InstallationError(msg)
# Treating it as code that has already been checked out
url_no_extras = path_to_url(url_no_extras)
if url_no_extras.lower().startswith('file:'):
if url_no_extras.lower().startswith("file:"):
package_name = Link(url_no_extras).egg_fragment
if extras:
return (
package_name,
url_no_extras,
Requirement("placeholder" + extras.lower()).extras,
get_requirement("placeholder" + extras.lower()).extras,
)
else:
return package_name, url_no_extras, set()
for version_control in vcs:
if url.lower().startswith(f'{version_control}:'):
url = f'{version_control}+{url}'
if url.lower().startswith(f"{version_control}:"):
url = f"{version_control}+{url}"
break
link = Link(url)
@@ -114,9 +98,9 @@ def parse_editable(editable_req: str) -> Tuple[Optional[str], str, Set[str]]:
if not link.is_vcs:
backends = ", ".join(vcs.all_schemes)
raise InstallationError(
f'{editable_req} is not a valid editable requirement. '
f'It should either be a path to a local project or a VCS URL '
f'(beginning with {backends}).'
f"{editable_req} is not a valid editable requirement. "
f"It should either be a path to a local project or a VCS URL "
f"(beginning with {backends})."
)
package_name = link.egg_fragment
@@ -128,43 +112,66 @@ def parse_editable(editable_req: str) -> Tuple[Optional[str], str, Set[str]]:
return package_name, url, set()
def check_first_requirement_in_file(filename: str) -> None:
"""Check if file is parsable as a requirements file.
This is heavily based on ``pkg_resources.parse_requirements``, but
simplified to just check the first meaningful line.
:raises InvalidRequirement: If the first meaningful line cannot be parsed
as an requirement.
"""
with open(filename, encoding="utf-8", errors="ignore") as f:
# Create a steppable iterator, so we can handle \-continuations.
lines = (
line
for line in (line.strip() for line in f)
if line and not line.startswith("#") # Skip blank lines/comments.
)
for line in lines:
# Drop comments -- a hash without a space may be in a URL.
if " #" in line:
line = line[: line.find(" #")]
# If there is a line continuation, drop it, and append the next line.
if line.endswith("\\"):
line = line[:-2].strip() + next(lines, "")
Requirement(line)
return
def deduce_helpful_msg(req: str) -> str:
"""Returns helpful msg in case requirements file does not exist,
or cannot be parsed.
:params req: Requirements file path
"""
msg = ""
if os.path.exists(req):
msg = " The path does exist. "
# Try to parse and check if it is a requirements file.
try:
with open(req) as fp:
# parse first line only
next(parse_requirements(fp.read()))
msg += (
"The argument you provided "
"({}) appears to be a"
" requirements file. If that is the"
" case, use the '-r' flag to install"
" the packages specified within it."
).format(req)
except RequirementParseError:
logger.debug(
"Cannot parse '%s' as requirements file", req, exc_info=True
)
if not os.path.exists(req):
return f" File '{req}' does not exist."
msg = " The path does exist. "
# Try to parse and check if it is a requirements file.
try:
check_first_requirement_in_file(req)
except InvalidRequirement:
logger.debug("Cannot parse '%s' as requirements file", req)
else:
msg += f" File '{req}' does not exist."
msg += (
f"The argument you provided "
f"({req}) appears to be a"
f" requirements file. If that is the"
f" case, use the '-r' flag to install"
f" the packages specified within it."
)
return msg
class RequirementParts:
def __init__(
self,
requirement: Optional[Requirement],
link: Optional[Link],
markers: Optional[Marker],
extras: Set[str],
self,
requirement: Optional[Requirement],
link: Optional[Link],
markers: Optional[Marker],
extras: Set[str],
):
self.requirement = requirement
self.link = link
@@ -199,6 +206,7 @@ def install_req_from_editable(
options: Optional[Dict[str, Any]] = None,
constraint: bool = False,
user_supplied: bool = False,
permit_editable_wheels: bool = False,
) -> InstallRequirement:
parts = parse_req_from_editable(editable_req)
@@ -208,6 +216,7 @@ def install_req_from_editable(
comes_from=comes_from,
user_supplied=user_supplied,
editable=True,
permit_editable_wheels=permit_editable_wheels,
link=parts.link,
constraint=constraint,
use_pep517=use_pep517,
@@ -250,6 +259,8 @@ def _get_url_from_path(path: str, name: str) -> Optional[str]:
if _looks_like_path(name) and os.path.isdir(path):
if is_installable_dir(path):
return path_to_url(path)
# TODO: The is_installable_dir test here might not be necessary
# now that it is done in load_pyproject_toml too.
raise InstallationError(
f"Directory {name!r} is not installable. Neither 'setup.py' "
"nor 'pyproject.toml' found."
@@ -258,24 +269,23 @@ def _get_url_from_path(path: str, name: str) -> Optional[str]:
return None
if os.path.isfile(path):
return path_to_url(path)
urlreq_parts = name.split('@', 1)
urlreq_parts = name.split("@", 1)
if len(urlreq_parts) >= 2 and not _looks_like_path(urlreq_parts[0]):
# If the path contains '@' and the part before it does not look
# like a path, try to treat it as a PEP 440 URL req instead.
return None
logger.warning(
'Requirement %r looks like a filename, but the '
'file does not exist',
name
"Requirement %r looks like a filename, but the file does not exist",
name,
)
return path_to_url(path)
def parse_req_from_line(name: str, line_source: Optional[str]) -> RequirementParts:
if is_url(name):
marker_sep = '; '
marker_sep = "; "
else:
marker_sep = ';'
marker_sep = ";"
if marker_sep in name:
name, markers_as_string = name.split(marker_sep, 1)
markers_as_string = markers_as_string.strip()
@@ -302,9 +312,8 @@ def parse_req_from_line(name: str, line_source: Optional[str]) -> RequirementPar
# it's a local file, dir, or url
if link:
# Handle relative file URLs
if link.scheme == 'file' and re.search(r'\.\./', link.url):
link = Link(
path_to_url(os.path.normpath(os.path.abspath(link.path))))
if link.scheme == "file" and re.search(r"\.\./", link.url):
link = Link(path_to_url(os.path.normpath(os.path.abspath(link.path))))
# wheel file
if link.is_wheel:
wheel = Wheel(link.filename) # can raise InvalidWheelFilename
@@ -323,25 +332,24 @@ def parse_req_from_line(name: str, line_source: Optional[str]) -> RequirementPar
def with_source(text: str) -> str:
if not line_source:
return text
return f'{text} (from {line_source})'
return f"{text} (from {line_source})"
def _parse_req_string(req_as_string: str) -> Requirement:
try:
req = Requirement(req_as_string)
req = get_requirement(req_as_string)
except InvalidRequirement:
if os.path.sep in req_as_string:
add_msg = "It looks like a path."
add_msg += deduce_helpful_msg(req_as_string)
elif ('=' in req_as_string and
not any(op in req_as_string for op in operators)):
elif "=" in req_as_string and not any(
op in req_as_string for op in operators
):
add_msg = "= is not a valid operator. Did you mean == ?"
else:
add_msg = ''
msg = with_source(
f'Invalid requirement: {req_as_string!r}'
)
add_msg = ""
msg = with_source(f"Invalid requirement: {req_as_string!r}")
if add_msg:
msg += f'\nHint: {add_msg}'
msg += f"\nHint: {add_msg}"
raise InstallationError(msg)
else:
# Deprecate extras after specifiers: "name>=1.0[extras]"
@@ -350,7 +358,7 @@ def parse_req_from_line(name: str, line_source: Optional[str]) -> RequirementPar
# RequirementParts
for spec in req.specifier:
spec_str = str(spec)
if spec_str.endswith(']'):
if spec_str.endswith("]"):
msg = f"Extras after version '{spec_str}'."
raise InstallationError(msg)
return req
@@ -382,8 +390,12 @@ def install_req_from_line(
parts = parse_req_from_line(name, line_source)
return InstallRequirement(
parts.requirement, comes_from, link=parts.link, markers=parts.markers,
use_pep517=use_pep517, isolated=isolated,
parts.requirement,
comes_from,
link=parts.link,
markers=parts.markers,
use_pep517=use_pep517,
isolated=isolated,
install_options=options.get("install_options", []) if options else [],
global_options=options.get("global_options", []) if options else [],
hash_options=options.get("hashes", {}) if options else {},
@@ -401,7 +413,7 @@ def install_req_from_req_string(
user_supplied: bool = False,
) -> InstallRequirement:
try:
req = Requirement(req_string)
req = get_requirement(req_string)
except InvalidRequirement:
raise InstallationError(f"Invalid requirement: '{req_string}'")
@@ -409,8 +421,12 @@ def install_req_from_req_string(
PyPI.file_storage_domain,
TestPyPI.file_storage_domain,
]
if (req.url and comes_from and comes_from.link and
comes_from.link.netloc in domains_not_allowed):
if (
req.url
and comes_from
and comes_from.link
and comes_from.link.netloc in domains_not_allowed
):
# Explicitly disallow pypi packages that depend on external urls
raise InstallationError(
"Packages installed from PyPI cannot depend on packages "
+43 -35
View File
@@ -8,7 +8,17 @@ import re
import shlex
import urllib.parse
from optparse import Values
from typing import TYPE_CHECKING, Any, Callable, Dict, Iterator, List, Optional, Tuple
from typing import (
TYPE_CHECKING,
Any,
Callable,
Dict,
Iterable,
Iterator,
List,
Optional,
Tuple,
)
from pipenv.patched.notpip._internal.cli import cmdoptions
from pipenv.patched.notpip._internal.exceptions import InstallationError, RequirementsFileParseError
@@ -25,20 +35,20 @@ if TYPE_CHECKING:
from pipenv.patched.notpip._internal.index.package_finder import PackageFinder
__all__ = ['parse_requirements']
__all__ = ["parse_requirements"]
ReqFileLines = Iterator[Tuple[int, str]]
ReqFileLines = Iterable[Tuple[int, str]]
LineParser = Callable[[str], Tuple[str, Values]]
SCHEME_RE = re.compile(r'^(http|https|file):', re.I)
COMMENT_RE = re.compile(r'(^|\s+)#.*$')
SCHEME_RE = re.compile(r"^(http|https|file):", re.I)
COMMENT_RE = re.compile(r"(^|\s+)#.*$")
# Matches environment variable-style values in '${MY_VARIABLE_1}' with the
# variable name consisting of only uppercase letters, digits or the '_'
# (underscore). This follows the POSIX standard defined in IEEE Std 1003.1,
# 2013 Edition.
ENV_VAR_RE = re.compile(r'(?P<var>\$\{(?P<name>[A-Z0-9_]+)\})')
ENV_VAR_RE = re.compile(r"(?P<var>\$\{(?P<name>[A-Z0-9_]+)\})")
SUPPORTED_OPTIONS: List[Callable[..., optparse.Option]] = [
cmdoptions.index_url,
@@ -134,10 +144,7 @@ def parse_requirements(
for parsed_line in parser.parse(filename, constraint):
parsed_req = handle_line(
parsed_line,
options=options,
finder=finder,
session=session
parsed_line, options=options, finder=finder, session=session
)
if parsed_req is not None:
yield parsed_req
@@ -161,8 +168,10 @@ def handle_requirement_line(
) -> ParsedRequirement:
# preserve for the nested code path
line_comes_from = '{} {} (line {})'.format(
'-c' if line.constraint else '-r', line.filename, line.lineno,
line_comes_from = "{} {} (line {})".format(
"-c" if line.constraint else "-r",
line.filename,
line.lineno,
)
assert line.is_requirement
@@ -187,7 +196,7 @@ def handle_requirement_line(
if dest in line.opts.__dict__ and line.opts.__dict__[dest]:
req_options[dest] = line.opts.__dict__[dest]
line_source = f'line {line.lineno} of {line.filename}'
line_source = f"line {line.lineno} of {line.filename}"
return ParsedRequirement(
requirement=line.requirement,
is_editable=line.is_editable,
@@ -213,8 +222,7 @@ def handle_option_line(
options.require_hashes = opts.require_hashes
if opts.features_enabled:
options.features_enabled.extend(
f for f in opts.features_enabled
if f not in options.features_enabled
f for f in opts.features_enabled if f not in options.features_enabled
)
# set finder options
@@ -254,7 +262,7 @@ def handle_option_line(
if session:
for host in opts.trusted_hosts or []:
source = f'line {lineno} of {filename}'
source = f"line {lineno} of {filename}"
session.add_trusted_host(host, source=source)
@@ -312,17 +320,15 @@ class RequirementsFileParser:
self._line_parser = line_parser
def parse(self, filename: str, constraint: bool) -> Iterator[ParsedLine]:
"""Parse a given file, yielding parsed lines.
"""
"""Parse a given file, yielding parsed lines."""
yield from self._parse_and_recurse(filename, constraint)
def _parse_and_recurse(
self, filename: str, constraint: bool
) -> Iterator[ParsedLine]:
for line in self._parse_file(filename, constraint):
if (
not line.is_requirement and
(line.opts.requirements or line.opts.constraints)
if not line.is_requirement and (
line.opts.requirements or line.opts.constraints
):
# parse a nested requirements file
if line.opts.requirements:
@@ -340,7 +346,8 @@ class RequirementsFileParser:
elif not SCHEME_RE.search(req_path):
# do a join so relative paths work
req_path = os.path.join(
os.path.dirname(filename), req_path,
os.path.dirname(filename),
req_path,
)
yield from self._parse_and_recurse(req_path, nested_constraint)
@@ -357,7 +364,7 @@ class RequirementsFileParser:
args_str, opts = self._line_parser(line)
except OptionParsingError as e:
# add offending line
msg = f'Invalid requirement: {line}\n{e.msg}'
msg = f"Invalid requirement: {line}\n{e.msg}"
raise RequirementsFileParseError(msg)
yield ParsedLine(
@@ -393,16 +400,16 @@ def break_args_options(line: str) -> Tuple[str, str]:
(and then optparse) the options, not the args. args can contain markers
which are corrupted by shlex.
"""
tokens = line.split(' ')
tokens = line.split(" ")
args = []
options = tokens[:]
for token in tokens:
if token.startswith('-') or token.startswith('--'):
if token.startswith("-") or token.startswith("--"):
break
else:
args.append(token)
options.pop(0)
return ' '.join(args), ' '.join(options)
return " ".join(args), " ".join(options)
class OptionParsingError(Exception):
@@ -425,6 +432,7 @@ def build_parser() -> optparse.OptionParser:
# that in our own exception.
def parser_exit(self: Any, msg: str) -> "NoReturn":
raise OptionParsingError(msg)
# NOTE: mypy disallows assigning to a method
# https://github.com/python/mypy/issues/2427
parser.exit = parser_exit # type: ignore
@@ -439,26 +447,26 @@ def join_lines(lines_enum: ReqFileLines) -> ReqFileLines:
primary_line_number = None
new_line: List[str] = []
for line_number, line in lines_enum:
if not line.endswith('\\') or COMMENT_RE.match(line):
if not line.endswith("\\") or COMMENT_RE.match(line):
if COMMENT_RE.match(line):
# this ensures comments are always matched later
line = ' ' + line
line = " " + line
if new_line:
new_line.append(line)
assert primary_line_number is not None
yield primary_line_number, ''.join(new_line)
yield primary_line_number, "".join(new_line)
new_line = []
else:
yield line_number, line
else:
if not new_line:
primary_line_number = line_number
new_line.append(line.strip('\\'))
new_line.append(line.strip("\\"))
# last line contains \
if new_line:
assert primary_line_number is not None
yield primary_line_number, ''.join(new_line)
yield primary_line_number, "".join(new_line)
# TODO: handle space after '\'.
@@ -468,7 +476,7 @@ def ignore_comments(lines_enum: ReqFileLines) -> ReqFileLines:
Strips comments and filter empty lines.
"""
for line_number, line in lines_enum:
line = COMMENT_RE.sub('', line)
line = COMMENT_RE.sub("", line)
line = line.strip()
if line:
yield line_number, line
@@ -512,15 +520,15 @@ def get_file_content(url: str, session: PipSession) -> Tuple[str, str]:
scheme = get_url_scheme(url)
# Pip has special support for file:// URLs (LocalFSAdapter).
if scheme in ['http', 'https', 'file']:
if scheme in ["http", "https", "file"]:
resp = session.get(url)
raise_for_status(resp)
return resp.url, resp.text
# Assume this is a bare path.
try:
with open(url, 'rb') as f:
with open(url, "rb") as f:
content = auto_decode(f.read())
except OSError as exc:
raise InstallationError(f'Could not open requirements file: {exc}')
raise InstallationError(f"Could not open requirements file: {exc}")
return url, content
+187 -175
View File
@@ -1,15 +1,15 @@
# The following comment should be removed at some point in the future.
# mypy: strict-optional=False
import functools
import logging
import os
import shutil
import sys
import uuid
import zipfile
from typing import Any, Dict, Iterable, List, Optional, Sequence, Union
from typing import Any, Collection, Dict, Iterable, List, Optional, Sequence, Union
from pipenv.patched.notpip._vendor import pkg_resources, six
from pipenv.patched.notpip._vendor.packaging.markers import Marker
from pipenv.patched.notpip._vendor.packaging.requirements import Requirement
from pipenv.patched.notpip._vendor.packaging.specifiers import SpecifierSet
@@ -17,39 +17,43 @@ from pipenv.patched.notpip._vendor.packaging.utils import canonicalize_name
from pipenv.patched.notpip._vendor.packaging.version import Version
from pipenv.patched.notpip._vendor.packaging.version import parse as parse_version
from pipenv.patched.notpip._vendor.pep517.wrappers import Pep517HookCaller
from pipenv.patched.notpip._vendor.pkg_resources import Distribution
from pipenv.patched.notpip._internal.build_env import BuildEnvironment, NoOpBuildEnvironment
from pipenv.patched.notpip._internal.exceptions import InstallationError
from pipenv.patched.notpip._internal.exceptions import InstallationError, LegacyInstallFailure
from pipenv.patched.notpip._internal.locations import get_scheme
from pipenv.patched.notpip._internal.metadata import (
BaseDistribution,
get_default_environment,
get_directory_distribution,
)
from pipenv.patched.notpip._internal.models.link import Link
from pipenv.patched.notpip._internal.operations.build.metadata import generate_metadata
from pipenv.patched.notpip._internal.operations.build.metadata_editable import generate_editable_metadata
from pipenv.patched.notpip._internal.operations.build.metadata_legacy import (
generate_metadata as generate_metadata_legacy,
)
from pipenv.patched.notpip._internal.operations.install.editable_legacy import (
install_editable as install_editable_legacy,
)
from pipenv.patched.notpip._internal.operations.install.legacy import LegacyInstallFailure
from pipenv.patched.notpip._internal.operations.install.legacy import install as install_legacy
from pipenv.patched.notpip._internal.operations.install.wheel import install_wheel
from pipenv.patched.notpip._internal.pyproject import load_pyproject_toml, make_pyproject_path
from pipenv.patched.notpip._internal.req.req_uninstall import UninstallPathSet
from pipenv.patched.notpip._internal.utils.deprecation import deprecated
from pipenv.patched.notpip._internal.utils.direct_url_helpers import direct_url_from_link
from pipenv.patched.notpip._internal.utils.direct_url_helpers import (
direct_url_for_editable,
direct_url_from_link,
)
from pipenv.patched.notpip._internal.utils.hashes import Hashes
from pipenv.patched.notpip._internal.utils.logging import indent_log
from pipenv.patched.notpip._internal.utils.misc import (
ask_path_exists,
backup_dir,
display_path,
dist_in_site_packages,
dist_in_usersite,
get_distribution,
hide_url,
redact_auth_from_url,
)
from pipenv.patched.notpip._internal.utils.packaging import get_metadata
from pipenv.patched.notpip._internal.utils.packaging import safe_extra
from pipenv.patched.notpip._internal.utils.subprocess import runner_with_spinner_message
from pipenv.patched.notpip._internal.utils.temp_dir import TempDirectory, tempdir_kinds
from pipenv.patched.notpip._internal.utils.virtualenv import running_under_virtualenv
from pipenv.patched.notpip._internal.vcs import vcs
@@ -57,32 +61,6 @@ from pipenv.patched.notpip._internal.vcs import vcs
logger = logging.getLogger(__name__)
def _get_dist(metadata_directory: str) -> Distribution:
"""Return a pkg_resources.Distribution for the provided
metadata directory.
"""
dist_dir = metadata_directory.rstrip(os.sep)
# Build a PathMetadata object, from path to metadata. :wink:
base_dir, dist_dir_name = os.path.split(dist_dir)
metadata = pkg_resources.PathMetadata(base_dir, dist_dir)
# Determine the correct Distribution object type.
if dist_dir.endswith(".egg-info"):
dist_cls = pkg_resources.Distribution
dist_name = os.path.splitext(dist_dir_name)[0]
else:
assert dist_dir.endswith(".dist-info")
dist_cls = pkg_resources.DistInfoDistribution
dist_name = os.path.splitext(dist_dir_name)[0].split("-")[0]
return dist_cls(
base_dir,
project_name=dist_name,
metadata=metadata,
)
class InstallRequirement:
"""
Represents something that may be installed later on, may have information
@@ -103,14 +81,16 @@ class InstallRequirement:
global_options: Optional[List[str]] = None,
hash_options: Optional[Dict[str, List[str]]] = None,
constraint: bool = False,
extras: Iterable[str] = (),
extras: Collection[str] = (),
user_supplied: bool = False,
permit_editable_wheels: bool = False,
) -> None:
assert req is None or isinstance(req, Requirement), req
self.req = req
self.comes_from = comes_from
self.constraint = constraint
self.editable = editable
self.permit_editable_wheels = permit_editable_wheels
self.legacy_install_reason: Optional[int] = None
# source_dir is the local directory where the linked requirement is
@@ -122,9 +102,7 @@ class InstallRequirement:
if self.editable:
assert link
if link.is_file:
self.source_dir = os.path.normpath(
os.path.abspath(link.file_path)
)
self.source_dir = os.path.normpath(os.path.abspath(link.file_path))
if link is None and req and req.url:
# PEP 508 URL requirement
@@ -140,18 +118,15 @@ class InstallRequirement:
if extras:
self.extras = extras
elif req:
self.extras = {
pkg_resources.safe_extra(extra) for extra in req.extras
}
self.extras = {safe_extra(extra) for extra in req.extras}
else:
self.extras = set()
if markers is None and req:
markers = req.marker
self.markers = markers
# This holds the pkg_resources.Distribution object if this requirement
# is already available:
self.satisfied_by: Optional[Distribution] = None
# This holds the Distribution object if this requirement is already installed.
self.satisfied_by: Optional[BaseDistribution] = None
# Whether the installation process should try to uninstall an existing
# distribution before installing this requirement.
self.should_reinstall = False
@@ -202,36 +177,34 @@ class InstallRequirement:
if self.req:
s = str(self.req)
if self.link:
s += ' from {}'.format(redact_auth_from_url(self.link.url))
s += " from {}".format(redact_auth_from_url(self.link.url))
elif self.link:
s = redact_auth_from_url(self.link.url)
else:
s = '<InstallRequirement>'
s = "<InstallRequirement>"
if self.satisfied_by is not None:
s += ' in {}'.format(display_path(self.satisfied_by.location))
s += " in {}".format(display_path(self.satisfied_by.location))
if self.comes_from:
if isinstance(self.comes_from, str):
comes_from: Optional[str] = self.comes_from
else:
comes_from = self.comes_from.from_path()
if comes_from:
s += f' (from {comes_from})'
s += f" (from {comes_from})"
return s
def __repr__(self) -> str:
return '<{} object: {} editable={!r}>'.format(
self.__class__.__name__, str(self), self.editable)
return "<{} object: {} editable={!r}>".format(
self.__class__.__name__, str(self), self.editable
)
def format_debug(self) -> str:
"""An un-tested helper for getting state, for debugging.
"""
"""An un-tested helper for getting state, for debugging."""
attributes = vars(self)
names = sorted(attributes)
state = (
"{}={!r}".format(attr, attributes[attr]) for attr in sorted(names)
)
return '<{name} object: {{{state}}}>'.format(
state = ("{}={!r}".format(attr, attributes[attr]) for attr in sorted(names))
return "<{name} object: {{{state}}}>".format(
name=self.__class__.__name__,
state=", ".join(state),
)
@@ -241,7 +214,19 @@ class InstallRequirement:
def name(self) -> Optional[str]:
if self.req is None:
return None
return pkg_resources.safe_name(self.req.name)
return self.req.name
@functools.lru_cache() # use cached_property in python 3.8+
def supports_pyproject_editable(self) -> bool:
if not self.use_pep517:
return False
assert self.pep517_backend
with self.build_env:
runner = runner_with_spinner_message(
"Checking if build backend supports build_editable"
)
with self.pep517_backend.subprocess_runner(runner):
return "build_editable" in self.pep517_backend._supported_features()
@property
def specifier(self) -> SpecifierSet:
@@ -254,18 +239,17 @@ class InstallRequirement:
For example, some-package==1.2 is pinned; some-package>1.2 is not.
"""
specifiers = self.specifier
return (len(specifiers) == 1 and
next(iter(specifiers)).operator in {'==', '==='})
return len(specifiers) == 1 and next(iter(specifiers)).operator in {"==", "==="}
def match_markers(self, extras_requested: Optional[Iterable[str]] = None) -> bool:
if not extras_requested:
# Provide an extra to safely evaluate the markers
# without matching any extra
extras_requested = ('',)
extras_requested = ("",)
if self.markers is not None:
return any(
self.markers.evaluate({'extra': extra})
for extra in extras_requested)
self.markers.evaluate({"extra": extra}) for extra in extras_requested
)
else:
return True
@@ -301,8 +285,7 @@ class InstallRequirement:
return Hashes(good_hashes)
def from_path(self) -> Optional[str]:
"""Format a nice indicator to show where this "comes from"
"""
"""Format a nice indicator to show where this "comes from" """
if self.req is None:
return None
s = str(self.req)
@@ -312,7 +295,7 @@ class InstallRequirement:
else:
comes_from = self.comes_from.from_path()
if comes_from:
s += '->' + comes_from
s += "->" + comes_from
return s
def ensure_build_location(
@@ -345,7 +328,7 @@ class InstallRequirement:
# FIXME: Is there a better place to create the build_dir? (hg and bzr
# need this)
if not os.path.exists(build_dir):
logger.debug('Creating directory %s', build_dir)
logger.debug("Creating directory %s", build_dir)
os.makedirs(build_dir)
actual_build_dir = os.path.join(build_dir, dir_name)
# `None` indicates that we respect the globally-configured deletion
@@ -359,8 +342,7 @@ class InstallRequirement:
).path
def _set_requirement(self) -> None:
"""Set requirement after generating metadata.
"""
"""Set requirement after generating metadata."""
assert self.req is None
assert self.metadata is not None
assert self.source_dir is not None
@@ -372,11 +354,13 @@ class InstallRequirement:
op = "==="
self.req = Requirement(
"".join([
self.metadata["Name"],
op,
self.metadata["Version"],
])
"".join(
[
self.metadata["Name"],
op,
self.metadata["Version"],
]
)
)
def warn_on_mismatching_name(self) -> None:
@@ -387,10 +371,12 @@ class InstallRequirement:
# If we're here, there's a mismatch. Log a warning about it.
logger.warning(
'Generating metadata for package %s '
'produced metadata for project name %s. Fix your '
'#egg=%s fragments.',
self.name, metadata_name, self.name
"Generating metadata for package %s "
"produced metadata for project name %s. Fix your "
"#egg=%s fragments.",
self.name,
metadata_name,
self.name,
)
self.req = Requirement(metadata_name)
@@ -401,30 +387,24 @@ class InstallRequirement:
"""
if self.req is None:
return
existing_dist = get_distribution(self.req.name)
existing_dist = get_default_environment().get_distribution(self.req.name)
if not existing_dist:
return
# pkg_resouces may contain a different copy of packaging.version from
# pip in if the downstream distributor does a poor job debundling pip.
# We avoid existing_dist.parsed_version and let SpecifierSet.contains
# parses the version instead.
existing_version = existing_dist.version
version_compatible = (
existing_version is not None and
self.req.specifier.contains(existing_version, prereleases=True)
version_compatible = self.req.specifier.contains(
existing_dist.version,
prereleases=True,
)
if not version_compatible:
self.satisfied_by = None
if use_user_site:
if dist_in_usersite(existing_dist):
if existing_dist.in_usersite:
self.should_reinstall = True
elif (running_under_virtualenv() and
dist_in_site_packages(existing_dist)):
elif running_under_virtualenv() and existing_dist.in_site_packages:
raise InstallationError(
"Will not install to the user site because it will "
"lack sys.path precedence to {} in {}".format(
existing_dist.project_name, existing_dist.location)
f"Will not install to the user site because it will "
f"lack sys.path precedence to {existing_dist.raw_name} "
f"in {existing_dist.location}"
)
else:
self.should_reinstall = True
@@ -448,16 +428,23 @@ class InstallRequirement:
@property
def unpacked_source_directory(self) -> str:
return os.path.join(
self.source_dir,
self.link and self.link.subdirectory_fragment or '')
self.source_dir, self.link and self.link.subdirectory_fragment or ""
)
@property
def setup_py_path(self) -> str:
assert self.source_dir, f"No source dir for {self}"
setup_py = os.path.join(self.unpacked_source_directory, 'setup.py')
setup_py = os.path.join(self.unpacked_source_directory, "setup.py")
return setup_py
@property
def setup_cfg_path(self) -> str:
assert self.source_dir, f"No source dir for {self}"
setup_cfg = os.path.join(self.unpacked_source_directory, "setup.cfg")
return setup_cfg
@property
def pyproject_toml_path(self) -> str:
assert self.source_dir, f"No source dir for {self}"
@@ -472,10 +459,7 @@ class InstallRequirement:
follow the PEP 517 or legacy (setup.py) code path.
"""
pyproject_toml_data = load_pyproject_toml(
self.use_pep517,
self.pyproject_toml_path,
self.setup_py_path,
str(self)
self.use_pep517, self.pyproject_toml_path, self.setup_py_path, str(self)
)
if pyproject_toml_data is None:
@@ -487,46 +471,68 @@ class InstallRequirement:
self.requirements_to_check = check
self.pyproject_requires = requires
self.pep517_backend = Pep517HookCaller(
self.unpacked_source_directory, backend, backend_path=backend_path,
self.unpacked_source_directory,
backend,
backend_path=backend_path,
python_executable=os.getenv('PIP_PYTHON_PATH', sys.executable)
)
def _generate_metadata(self) -> str:
"""Invokes metadata generator functions, with the required arguments.
def isolated_editable_sanity_check(self) -> None:
"""Check that an editable requirement if valid for use with PEP 517/518.
This verifies that an editable that has a pyproject.toml either supports PEP 660
or as a setup.py or a setup.cfg
"""
if not self.use_pep517:
assert self.unpacked_source_directory
if not os.path.exists(self.setup_py_path):
raise InstallationError(
f'File "setup.py" not found for legacy project {self}.'
)
return generate_metadata_legacy(
build_env=self.build_env,
setup_py_path=self.setup_py_path,
source_dir=self.unpacked_source_directory,
isolated=self.isolated,
details=self.name or f"from {self.link}"
if (
self.editable
and self.use_pep517
and not self.supports_pyproject_editable()
and not os.path.isfile(self.setup_py_path)
and not os.path.isfile(self.setup_cfg_path)
):
raise InstallationError(
f"Project {self} has a 'pyproject.toml' and its build "
f"backend is missing the 'build_editable' hook. Since it does not "
f"have a 'setup.py' nor a 'setup.cfg', "
f"it cannot be installed in editable mode. "
f"Consider using a build backend that supports PEP 660."
)
assert self.pep517_backend is not None
return generate_metadata(
build_env=self.build_env,
backend=self.pep517_backend,
)
def prepare_metadata(self) -> None:
"""Ensure that project metadata is available.
Under PEP 517, call the backend hook to prepare the metadata.
Under PEP 517 and PEP 660, call the backend hook to prepare the metadata.
Under legacy processing, call setup.py egg-info.
"""
assert self.source_dir
details = self.name or f"from {self.link}"
with indent_log():
self.metadata_directory = self._generate_metadata()
if self.use_pep517:
assert self.pep517_backend is not None
if (
self.editable
and self.permit_editable_wheels
and self.supports_pyproject_editable()
):
self.metadata_directory = generate_editable_metadata(
build_env=self.build_env,
backend=self.pep517_backend,
details=details,
)
else:
self.metadata_directory = generate_metadata(
build_env=self.build_env,
backend=self.pep517_backend,
details=details,
)
else:
self.metadata_directory = generate_metadata_legacy(
build_env=self.build_env,
setup_py_path=self.setup_py_path,
source_dir=self.unpacked_source_directory,
isolated=self.isolated,
details=details,
)
# Act on the newly generated metadata, based on the name and version.
if not self.name:
@@ -538,26 +544,26 @@ class InstallRequirement:
@property
def metadata(self) -> Any:
if not hasattr(self, '_metadata'):
self._metadata = get_metadata(self.get_dist())
if not hasattr(self, "_metadata"):
self._metadata = self.get_dist().metadata
return self._metadata
def get_dist(self) -> Distribution:
return _get_dist(self.metadata_directory)
def get_dist(self) -> BaseDistribution:
return get_directory_distribution(self.metadata_directory)
def assert_source_matches_version(self) -> None:
assert self.source_dir
version = self.metadata['version']
version = self.metadata["version"]
if self.req.specifier and version not in self.req.specifier:
logger.warning(
'Requested %s, but installing version %s',
"Requested %s, but installing version %s",
self,
version,
)
else:
logger.debug(
'Source in %s has version %s, which satisfies requirement %s',
"Source in %s has version %s, which satisfies requirement %s",
display_path(self.source_dir),
version,
self,
@@ -590,14 +596,13 @@ class InstallRequirement:
def update_editable(self) -> None:
if not self.link:
logger.debug(
"Cannot update repository at %s; repository location is "
"unknown",
"Cannot update repository at %s; repository location is unknown",
self.source_dir,
)
return
assert self.editable
assert self.source_dir
if self.link.scheme == 'file':
if self.link.scheme == "file":
# Static paths don't get updated
return
vcs_backend = vcs.get_backend_for_scheme(self.link.scheme)
@@ -605,7 +610,7 @@ class InstallRequirement:
# So here, if it's neither a path nor a valid VCS URL, it's a bug.
assert vcs_backend, f"Unsupported VCS URL {self.link.url}"
hidden_url = hide_url(self.link.url)
vcs_backend.obtain(self.source_dir, url=hidden_url)
vcs_backend.obtain(self.source_dir, url=hidden_url, verbosity=0)
# Top-level Actions
def uninstall(
@@ -624,29 +629,28 @@ class InstallRequirement:
"""
assert self.req
dist = get_distribution(self.req.name)
dist = get_default_environment().get_distribution(self.req.name)
if not dist:
logger.warning("Skipping %s as it is not installed.", self.name)
return None
logger.info('Found existing installation: %s', dist)
logger.info("Found existing installation: %s", dist)
uninstalled_pathset = UninstallPathSet.from_dist(dist)
uninstalled_pathset.remove(auto_confirm, verbose)
return uninstalled_pathset
def _get_archive_name(self, path: str, parentdir: str, rootdir: str) -> str:
def _clean_zip_name(name: str, prefix: str) -> str:
assert name.startswith(prefix + os.path.sep), (
f"name {name!r} doesn't start with prefix {prefix!r}"
)
name = name[len(prefix) + 1:]
name = name.replace(os.path.sep, '/')
assert name.startswith(
prefix + os.path.sep
), f"name {name!r} doesn't start with prefix {prefix!r}"
name = name[len(prefix) + 1 :]
name = name.replace(os.path.sep, "/")
return name
path = os.path.join(parentdir, path)
name = _clean_zip_name(path, rootdir)
return self.name + '/' + name
return self.name + "/" + name
def archive(self, build_dir: Optional[str]) -> None:
"""Saves archive to provided build_dir.
@@ -658,57 +662,62 @@ class InstallRequirement:
return
create_archive = True
archive_name = '{}-{}.zip'.format(self.name, self.metadata["version"])
archive_name = "{}-{}.zip".format(self.name, self.metadata["version"])
archive_path = os.path.join(build_dir, archive_name)
if os.path.exists(archive_path):
response = ask_path_exists(
'The file {} exists. (i)gnore, (w)ipe, '
'(b)ackup, (a)bort '.format(
display_path(archive_path)),
('i', 'w', 'b', 'a'))
if response == 'i':
"The file {} exists. (i)gnore, (w)ipe, "
"(b)ackup, (a)bort ".format(display_path(archive_path)),
("i", "w", "b", "a"),
)
if response == "i":
create_archive = False
elif response == 'w':
logger.warning('Deleting %s', display_path(archive_path))
elif response == "w":
logger.warning("Deleting %s", display_path(archive_path))
os.remove(archive_path)
elif response == 'b':
elif response == "b":
dest_file = backup_dir(archive_path)
logger.warning(
'Backing up %s to %s',
"Backing up %s to %s",
display_path(archive_path),
display_path(dest_file),
)
shutil.move(archive_path, dest_file)
elif response == 'a':
elif response == "a":
sys.exit(-1)
if not create_archive:
return
zip_output = zipfile.ZipFile(
archive_path, 'w', zipfile.ZIP_DEFLATED, allowZip64=True,
archive_path,
"w",
zipfile.ZIP_DEFLATED,
allowZip64=True,
)
with zip_output:
dir = os.path.normcase(
os.path.abspath(self.unpacked_source_directory)
)
dir = os.path.normcase(os.path.abspath(self.unpacked_source_directory))
for dirpath, dirnames, filenames in os.walk(dir):
for dirname in dirnames:
dir_arcname = self._get_archive_name(
dirname, parentdir=dirpath, rootdir=dir,
dirname,
parentdir=dirpath,
rootdir=dir,
)
zipdir = zipfile.ZipInfo(dir_arcname + '/')
zipdir = zipfile.ZipInfo(dir_arcname + "/")
zipdir.external_attr = 0x1ED << 16 # 0o755
zip_output.writestr(zipdir, '')
zip_output.writestr(zipdir, "")
for filename in filenames:
file_arcname = self._get_archive_name(
filename, parentdir=dirpath, rootdir=dir,
filename,
parentdir=dirpath,
rootdir=dir,
)
filename = os.path.join(dirpath, filename)
zip_output.write(filename, file_arcname)
logger.info('Saved %s', display_path(archive_path))
logger.info("Saved %s", display_path(archive_path))
def install(
self,
@@ -719,7 +728,7 @@ class InstallRequirement:
prefix: Optional[str] = None,
warn_script_location: bool = True,
use_user_site: bool = False,
pycompile: bool = True
pycompile: bool = True,
) -> None:
scheme = get_scheme(
self.name,
@@ -731,7 +740,7 @@ class InstallRequirement:
)
global_options = global_options if global_options is not None else []
if self.editable:
if self.editable and not self.is_wheel:
install_editable_legacy(
install_options,
global_options,
@@ -750,7 +759,9 @@ class InstallRequirement:
if self.is_wheel:
assert self.local_file_path
direct_url = None
if self.original_link:
if self.editable:
direct_url = direct_url_for_editable(self.unpacked_source_directory)
elif self.original_link:
direct_url = direct_url_from_link(
self.original_link,
self.source_dir,
@@ -798,7 +809,7 @@ class InstallRequirement:
)
except LegacyInstallFailure as exc:
self.install_succeeded = False
six.reraise(*exc.parent)
raise exc
except Exception:
self.install_succeeded = True
raise
@@ -809,8 +820,9 @@ class InstallRequirement:
deprecated(
reason=(
"{} was installed using the legacy 'setup.py install' "
"method, because a wheel could not be built for it.".
format(self.name)
"method, because a wheel could not be built for it.".format(
self.name
)
),
replacement="to fix the wheel build issue reported above",
gone_in=None,
+34 -35
View File
@@ -13,10 +13,8 @@ logger = logging.getLogger(__name__)
class RequirementSet:
def __init__(self, check_supported_wheels: bool = True) -> None:
"""Create a RequirementSet.
"""
"""Create a RequirementSet."""
self.requirements: Dict[str, InstallRequirement] = OrderedDict()
self.check_supported_wheels = check_supported_wheels
@@ -28,7 +26,7 @@ class RequirementSet:
(req for req in self.requirements.values() if not req.comes_from),
key=lambda req: canonicalize_name(req.name or ""),
)
return ' '.join(str(req.req) for req in requirements)
return " ".join(str(req.req) for req in requirements)
def __repr__(self) -> str:
requirements = sorted(
@@ -36,11 +34,11 @@ class RequirementSet:
key=lambda req: canonicalize_name(req.name or ""),
)
format_string = '<{classname} object; {count} requirement(s): {reqs}>'
format_string = "<{classname} object; {count} requirement(s): {reqs}>"
return format_string.format(
classname=self.__class__.__name__,
count=len(requirements),
reqs=', '.join(str(req.req) for req in requirements),
reqs=", ".join(str(req.req) for req in requirements),
)
def add_unnamed_requirement(self, install_req: InstallRequirement) -> None:
@@ -57,7 +55,7 @@ class RequirementSet:
self,
install_req: InstallRequirement,
parent_req_name: Optional[str] = None,
extras_requested: Optional[Iterable[str]] = None
extras_requested: Optional[Iterable[str]] = None,
) -> Tuple[List[InstallRequirement], Optional[InstallRequirement]]:
"""Add install_req as a requirement to install.
@@ -77,7 +75,8 @@ class RequirementSet:
if not install_req.match_markers(extras_requested):
logger.info(
"Ignoring %s: markers '%s' don't match your environment",
install_req.name, install_req.markers,
install_req.name,
install_req.markers,
)
return [], None
@@ -88,16 +87,17 @@ class RequirementSet:
if install_req.link and install_req.link.is_wheel:
wheel = Wheel(install_req.link.filename)
tags = compatibility_tags.get_supported()
if (self.check_supported_wheels and not wheel.supported(tags)):
if self.check_supported_wheels and not wheel.supported(tags):
raise InstallationError(
"{} is not a supported wheel on this platform.".format(
wheel.filename)
wheel.filename
)
)
# This next bit is really a sanity check.
assert not install_req.user_supplied or parent_req_name is None, (
"a user supplied req shouldn't have a parent"
)
assert (
not install_req.user_supplied or parent_req_name is None
), "a user supplied req shouldn't have a parent"
# Unnamed requirements are scanned again and the requirement won't be
# added as a dependency until after scanning.
@@ -107,23 +107,25 @@ class RequirementSet:
try:
existing_req: Optional[InstallRequirement] = self.get_requirement(
install_req.name)
install_req.name
)
except KeyError:
existing_req = None
has_conflicting_requirement = (
parent_req_name is None and
existing_req and
not existing_req.constraint and
existing_req.extras == install_req.extras and
existing_req.req and
install_req.req and
existing_req.req.specifier != install_req.req.specifier
parent_req_name is None
and existing_req
and not existing_req.constraint
and existing_req.extras == install_req.extras
and existing_req.req
and install_req.req
and existing_req.req.specifier != install_req.req.specifier
)
if has_conflicting_requirement:
raise InstallationError(
"Double requirement given: {} (already in {}, name={!r})"
.format(install_req, existing_req, install_req.name)
"Double requirement given: {} (already in {}, name={!r})".format(
install_req, existing_req, install_req.name
)
)
# When no existing requirement exists, add the requirement as a
@@ -138,12 +140,8 @@ class RequirementSet:
if install_req.constraint or not existing_req.constraint:
return [], existing_req
does_not_satisfy_constraint = (
install_req.link and
not (
existing_req.link and
install_req.link.path == existing_req.link.path
)
does_not_satisfy_constraint = install_req.link and not (
existing_req.link and install_req.link.path == existing_req.link.path
)
if does_not_satisfy_constraint:
raise InstallationError(
@@ -158,12 +156,13 @@ class RequirementSet:
# mark the existing object as such.
if install_req.user_supplied:
existing_req.user_supplied = True
existing_req.extras = tuple(sorted(
set(existing_req.extras) | set(install_req.extras)
))
existing_req.extras = tuple(
sorted(set(existing_req.extras) | set(install_req.extras))
)
logger.debug(
"Setting %s extras to: %s",
existing_req, existing_req.extras,
existing_req,
existing_req.extras,
)
# Return the existing requirement for addition to the parent and
# scanning again.
@@ -173,8 +172,8 @@ class RequirementSet:
project_name = canonicalize_name(name)
return (
project_name in self.requirements and
not self.requirements[project_name].constraint
project_name in self.requirements
and not self.requirements[project_name].constraint
)
def get_requirement(self, name: str) -> InstallRequirement:
@@ -40,12 +40,10 @@ def update_env_context_manager(**changes: str) -> Iterator[None]:
@contextlib.contextmanager
def get_requirement_tracker() -> Iterator["RequirementTracker"]:
root = os.environ.get('PIP_REQ_TRACKER')
root = os.environ.get("PIP_REQ_TRACKER")
with contextlib.ExitStack() as ctx:
if root is None:
root = ctx.enter_context(
TempDirectory(kind='req-tracker')
).path
root = ctx.enter_context(TempDirectory(kind="req-tracker")).path
ctx.enter_context(update_env_context_manager(PIP_REQ_TRACKER=root))
logger.debug("Initialized build tracking at %s", root)
@@ -54,7 +52,6 @@ def get_requirement_tracker() -> Iterator["RequirementTracker"]:
class RequirementTracker:
def __init__(self, root: str) -> None:
self._root = root
self._entries: Set[InstallRequirement] = set()
@@ -68,7 +65,7 @@ class RequirementTracker:
self,
exc_type: Optional[Type[BaseException]],
exc_val: Optional[BaseException],
exc_tb: Optional[TracebackType]
exc_tb: Optional[TracebackType],
) -> None:
self.cleanup()
@@ -77,8 +74,7 @@ class RequirementTracker:
return os.path.join(self._root, hashed)
def add(self, req: InstallRequirement) -> None:
"""Add an InstallRequirement to build tracking.
"""
"""Add an InstallRequirement to build tracking."""
assert req.link
# Get the file to write information about this requirement.
@@ -92,30 +88,28 @@ class RequirementTracker:
except FileNotFoundError:
pass
else:
message = '{} is already being built: {}'.format(
req.link, contents)
message = "{} is already being built: {}".format(req.link, contents)
raise LookupError(message)
# If we're here, req should really not be building already.
assert req not in self._entries
# Start tracking this requirement.
with open(entry_path, 'w', encoding="utf-8") as fp:
with open(entry_path, "w", encoding="utf-8") as fp:
fp.write(str(req))
self._entries.add(req)
logger.debug('Added %s to build tracker %r', req, self._root)
logger.debug("Added %s to build tracker %r", req, self._root)
def remove(self, req: InstallRequirement) -> None:
"""Remove an InstallRequirement from build tracking.
"""
"""Remove an InstallRequirement from build tracking."""
assert req.link
# Delete the created file and the corresponding entries.
os.unlink(self._entry_path(req.link))
self._entries.remove(req)
logger.debug('Removed %s from build tracker %r', req, self._root)
logger.debug("Removed %s from build tracker %r", req, self._root)
def cleanup(self) -> None:
for req in set(self._entries):
@@ -1,4 +1,3 @@
import csv
import functools
import os
import sys
@@ -6,47 +5,33 @@ import sysconfig
from importlib.util import cache_from_source
from typing import Any, Callable, Dict, Iterable, Iterator, List, Optional, Set, Tuple
from pipenv.patched.notpip._vendor import pkg_resources
from pipenv.patched.notpip._vendor.pkg_resources import Distribution
from pipenv.patched.notpip._internal.exceptions import UninstallationError
from pipenv.patched.notpip._internal.locations import get_bin_prefix, get_bin_user
from pipenv.patched.notpip._internal.metadata import BaseDistribution
from pipenv.patched.notpip._internal.utils.compat import WINDOWS
from pipenv.patched.notpip._internal.utils.egg_link import egg_link_path_from_location
from pipenv.patched.notpip._internal.utils.logging import getLogger, indent_log
from pipenv.patched.notpip._internal.utils.misc import (
ask,
dist_in_usersite,
dist_is_local,
egg_link_path,
is_local,
normalize_path,
renames,
rmtree,
)
from pipenv.patched.notpip._internal.utils.misc import ask, is_local, normalize_path, renames, rmtree
from pipenv.patched.notpip._internal.utils.temp_dir import AdjacentTempDirectory, TempDirectory
logger = getLogger(__name__)
def _script_names(dist: Distribution, script_name: str, is_gui: bool) -> List[str]:
def _script_names(bin_dir: str, script_name: str, is_gui: bool) -> Iterator[str]:
"""Create the fully qualified name of the files created by
{console,gui}_scripts for the given ``dist``.
Returns the list of file names
"""
if dist_in_usersite(dist):
bin_dir = get_bin_user()
else:
bin_dir = get_bin_prefix()
exe_name = os.path.join(bin_dir, script_name)
paths_to_remove = [exe_name]
if WINDOWS:
paths_to_remove.append(exe_name + '.exe')
paths_to_remove.append(exe_name + '.exe.manifest')
if is_gui:
paths_to_remove.append(exe_name + '-script.pyw')
else:
paths_to_remove.append(exe_name + '-script.py')
return paths_to_remove
yield exe_name
if not WINDOWS:
return
yield f"{exe_name}.exe"
yield f"{exe_name}.exe.manifest"
if is_gui:
yield f"{exe_name}-script.pyw"
else:
yield f"{exe_name}-script.py"
def _unique(fn: Callable[..., Iterator[Any]]) -> Callable[..., Iterator[Any]]:
@@ -57,11 +42,12 @@ def _unique(fn: Callable[..., Iterator[Any]]) -> Callable[..., Iterator[Any]]:
if item not in seen:
seen.add(item)
yield item
return unique
@_unique
def uninstallation_paths(dist: Distribution) -> Iterator[str]:
def uninstallation_paths(dist: BaseDistribution) -> Iterator[str]:
"""
Yield all the uninstallation paths for dist based on RECORD-without-.py[co]
@@ -75,30 +61,32 @@ def uninstallation_paths(dist: Distribution) -> Iterator[str]:
https://packaging.python.org/specifications/recording-installed-packages/
"""
try:
r = csv.reader(dist.get_metadata_lines('RECORD'))
except FileNotFoundError as missing_record_exception:
msg = 'Cannot uninstall {dist}, RECORD file not found.'.format(dist=dist)
try:
installer = next(dist.get_metadata_lines('INSTALLER'))
if not installer or installer == 'pip':
raise ValueError()
except (OSError, StopIteration, ValueError):
dep = '{}=={}'.format(dist.project_name, dist.version)
msg += (" You might be able to recover from this via: "
"'pip install --force-reinstall --no-deps {}'.".format(dep))
location = dist.location
assert location is not None, "not installed"
entries = dist.iter_declared_entries()
if entries is None:
msg = "Cannot uninstall {dist}, RECORD file not found.".format(dist=dist)
installer = dist.installer
if not installer or installer == "pip":
dep = "{}=={}".format(dist.raw_name, dist.version)
msg += (
" You might be able to recover from this via: "
"'pip install --force-reinstall --no-deps {}'.".format(dep)
)
else:
msg += ' Hint: The package was installed by {}.'.format(installer)
raise UninstallationError(msg) from missing_record_exception
for row in r:
path = os.path.join(dist.location, row[0])
msg += " Hint: The package was installed by {}.".format(installer)
raise UninstallationError(msg)
for entry in entries:
path = os.path.join(location, entry)
yield path
if path.endswith('.py'):
if path.endswith(".py"):
dn, fn = os.path.split(path)
base = fn[:-3]
path = os.path.join(dn, base + '.pyc')
path = os.path.join(dn, base + ".pyc")
yield path
path = os.path.join(dn, base + '.pyo')
path = os.path.join(dn, base + ".pyo")
yield path
@@ -112,8 +100,8 @@ def compact(paths: Iterable[str]) -> Set[str]:
short_paths: Set[str] = set()
for path in sorted(paths, key=len):
should_skip = any(
path.startswith(shortpath.rstrip("*")) and
path[len(shortpath.rstrip("*").rstrip(sep))] == sep
path.startswith(shortpath.rstrip("*"))
and path[len(shortpath.rstrip("*").rstrip(sep))] == sep
for shortpath in short_paths
)
if not should_skip:
@@ -136,18 +124,15 @@ def compress_for_rename(paths: Iterable[str]) -> Set[str]:
return os.path.normcase(os.path.join(*a))
for root in unchecked:
if any(os.path.normcase(root).startswith(w)
for w in wildcards):
if any(os.path.normcase(root).startswith(w) for w in wildcards):
# This directory has already been handled.
continue
all_files: Set[str] = set()
all_subdirs: Set[str] = set()
for dirname, subdirs, files in os.walk(root):
all_subdirs.update(norm_join(root, dirname, d)
for d in subdirs)
all_files.update(norm_join(root, dirname, f)
for f in files)
all_subdirs.update(norm_join(root, dirname, d) for d in subdirs)
all_files.update(norm_join(root, dirname, f) for f in files)
# If all the files we found are in our remaining set of files to
# remove, then remove them from the latter set and add a wildcard
# for the directory.
@@ -196,14 +181,14 @@ def compress_for_output_listing(paths: Iterable[str]) -> Tuple[Set[str], Set[str
continue
file_ = os.path.join(dirpath, fname)
if (os.path.isfile(file_) and
os.path.normcase(file_) not in _normcased_files):
if (
os.path.isfile(file_)
and os.path.normcase(file_) not in _normcased_files
):
# We are skipping this file. Add it to the set.
will_skip.add(file_)
will_remove = files | {
os.path.join(folder, "*") for folder in folders
}
will_remove = files | {os.path.join(folder, "*") for folder in folders}
return will_remove, will_skip
@@ -211,6 +196,7 @@ def compress_for_output_listing(paths: Iterable[str]) -> Tuple[Set[str], Set[str
class StashedUninstallPathSet:
"""A set of file rename operations to stash files while
tentatively uninstalling them."""
def __init__(self) -> None:
# Mapping from source file root to [Adjacent]TempDirectory
# for files under that directory.
@@ -252,7 +238,7 @@ class StashedUninstallPathSet:
else:
# Did not find any suitable root
head = os.path.dirname(path)
save_dir = TempDirectory(kind='uninstall')
save_dir = TempDirectory(kind="uninstall")
self._save_dirs[head] = save_dir
relpath = os.path.relpath(path, head)
@@ -271,7 +257,7 @@ class StashedUninstallPathSet:
new_path = self._get_file_stash(path)
self._moves.append((path, new_path))
if (path_is_dir and os.path.isdir(new_path)):
if path_is_dir and os.path.isdir(new_path):
# If we're moving a directory, we need to
# remove the destination first or else it will be
# moved to inside the existing directory.
@@ -295,7 +281,7 @@ class StashedUninstallPathSet:
for new_path, path in self._moves:
try:
logger.debug('Replacing %s from %s', new_path, path)
logger.debug("Replacing %s from %s", new_path, path)
if os.path.isfile(new_path) or os.path.islink(new_path):
os.unlink(new_path)
elif os.path.isdir(new_path):
@@ -315,11 +301,12 @@ class StashedUninstallPathSet:
class UninstallPathSet:
"""A set of file paths to be removed in the uninstallation of a
requirement."""
def __init__(self, dist: Distribution) -> None:
self.paths: Set[str] = set()
def __init__(self, dist: BaseDistribution) -> None:
self._paths: Set[str] = set()
self._refuse: Set[str] = set()
self.pth: Dict[str, UninstallPthEntries] = {}
self.dist = dist
self._pth: Dict[str, UninstallPthEntries] = {}
self._dist = dist
self._moved_paths = StashedUninstallPathSet()
def _permitted(self, path: str) -> bool:
@@ -340,58 +327,55 @@ class UninstallPathSet:
if not os.path.exists(path):
return
if self._permitted(path):
self.paths.add(path)
self._paths.add(path)
else:
self._refuse.add(path)
# __pycache__ files can show up after 'installed-files.txt' is created,
# due to imports
if os.path.splitext(path)[1] == '.py':
if os.path.splitext(path)[1] == ".py":
self.add(cache_from_source(path))
def add_pth(self, pth_file: str, entry: str) -> None:
pth_file = normalize_path(pth_file)
if self._permitted(pth_file):
if pth_file not in self.pth:
self.pth[pth_file] = UninstallPthEntries(pth_file)
self.pth[pth_file].add(entry)
if pth_file not in self._pth:
self._pth[pth_file] = UninstallPthEntries(pth_file)
self._pth[pth_file].add(entry)
else:
self._refuse.add(pth_file)
def remove(self, auto_confirm: bool = False, verbose: bool = False) -> None:
"""Remove paths in ``self.paths`` with confirmation (unless
"""Remove paths in ``self._paths`` with confirmation (unless
``auto_confirm`` is True)."""
if not self.paths:
if not self._paths:
logger.info(
"Can't uninstall '%s'. No files were found to uninstall.",
self.dist.project_name,
self._dist.raw_name,
)
return
dist_name_version = (
self.dist.project_name + "-" + self.dist.version
)
logger.info('Uninstalling %s:', dist_name_version)
dist_name_version = f"{self._dist.raw_name}-{self._dist.version}"
logger.info("Uninstalling %s:", dist_name_version)
with indent_log():
if auto_confirm or self._allowed_to_proceed(verbose):
moved = self._moved_paths
for_rename = compress_for_rename(self.paths)
for_rename = compress_for_rename(self._paths)
for path in sorted(compact(for_rename)):
moved.stash(path)
logger.verbose('Removing file or directory %s', path)
logger.verbose("Removing file or directory %s", path)
for pth in self.pth.values():
for pth in self._pth.values():
pth.remove()
logger.info('Successfully uninstalled %s', dist_name_version)
logger.info("Successfully uninstalled %s", dist_name_version)
def _allowed_to_proceed(self, verbose: bool) -> bool:
"""Display which files would be deleted and prompt for confirmation
"""
"""Display which files would be deleted and prompt for confirmation"""
def _display(msg: str, paths: Iterable[str]) -> None:
if not paths:
@@ -403,32 +387,32 @@ class UninstallPathSet:
logger.info(path)
if not verbose:
will_remove, will_skip = compress_for_output_listing(self.paths)
will_remove, will_skip = compress_for_output_listing(self._paths)
else:
# In verbose mode, display all the files that are going to be
# deleted.
will_remove = set(self.paths)
will_remove = set(self._paths)
will_skip = set()
_display('Would remove:', will_remove)
_display('Would not remove (might be manually added):', will_skip)
_display('Would not remove (outside of prefix):', self._refuse)
_display("Would remove:", will_remove)
_display("Would not remove (might be manually added):", will_skip)
_display("Would not remove (outside of prefix):", self._refuse)
if verbose:
_display('Will actually move:', compress_for_rename(self.paths))
_display("Will actually move:", compress_for_rename(self._paths))
return ask('Proceed (Y/n)? ', ('y', 'n', '')) != 'n'
return ask("Proceed (Y/n)? ", ("y", "n", "")) != "n"
def rollback(self) -> None:
"""Rollback the changes previously made by remove()."""
if not self._moved_paths.can_rollback:
logger.error(
"Can't roll back %s; was not uninstalled",
self.dist.project_name,
self._dist.raw_name,
)
return
logger.info('Rolling back uninstall of %s', self.dist.project_name)
logger.info("Rolling back uninstall of %s", self._dist.raw_name)
self._moved_paths.rollback()
for pth in self.pth.values():
for pth in self._pth.values():
pth.rollback()
def commit(self) -> None:
@@ -436,132 +420,156 @@ class UninstallPathSet:
self._moved_paths.commit()
@classmethod
def from_dist(cls, dist: Distribution) -> "UninstallPathSet":
dist_path = normalize_path(dist.location)
if not dist_is_local(dist):
def from_dist(cls, dist: BaseDistribution) -> "UninstallPathSet":
dist_location = dist.location
info_location = dist.info_location
if dist_location is None:
logger.info(
"Not uninstalling %s since it is not installed",
dist.canonical_name,
)
return cls(dist)
normalized_dist_location = normalize_path(dist_location)
if not dist.local:
logger.info(
"Not uninstalling %s at %s, outside environment %s",
dist.key,
dist_path,
dist.canonical_name,
normalized_dist_location,
sys.prefix,
)
return cls(dist)
if dist_path in {p for p in {sysconfig.get_path("stdlib"),
sysconfig.get_path("platstdlib")}
if p}:
if normalized_dist_location in {
p
for p in {sysconfig.get_path("stdlib"), sysconfig.get_path("platstdlib")}
if p
}:
logger.info(
"Not uninstalling %s at %s, as it is in the standard library.",
dist.key,
dist_path,
dist.canonical_name,
normalized_dist_location,
)
return cls(dist)
paths_to_remove = cls(dist)
develop_egg_link = egg_link_path(dist)
develop_egg_link_egg_info = '{}.egg-info'.format(
pkg_resources.to_filename(dist.project_name))
egg_info_exists = dist.egg_info and os.path.exists(dist.egg_info)
# Special case for distutils installed package
distutils_egg_info = getattr(dist._provider, 'path', None)
develop_egg_link = egg_link_path_from_location(dist.raw_name)
# Distribution is installed with metadata in a "flat" .egg-info
# directory. This means it is not a modern .dist-info installation, an
# egg, or legacy editable.
setuptools_flat_installation = (
dist.installed_with_setuptools_egg_info
and info_location is not None
and os.path.exists(info_location)
# If dist is editable and the location points to a ``.egg-info``,
# we are in fact in the legacy editable case.
and not info_location.endswith(f"{dist.setuptools_filename}.egg-info")
)
# Uninstall cases order do matter as in the case of 2 installs of the
# same package, pip needs to uninstall the currently detected version
if (egg_info_exists and dist.egg_info.endswith('.egg-info') and
not dist.egg_info.endswith(develop_egg_link_egg_info)):
# if dist.egg_info.endswith(develop_egg_link_egg_info), we
# are in fact in the develop_egg_link case
paths_to_remove.add(dist.egg_info)
if dist.has_metadata('installed-files.txt'):
for installed_file in dist.get_metadata(
'installed-files.txt').splitlines():
path = os.path.normpath(
os.path.join(dist.egg_info, installed_file)
)
paths_to_remove.add(path)
if setuptools_flat_installation:
if info_location is not None:
paths_to_remove.add(info_location)
installed_files = dist.iter_declared_entries()
if installed_files is not None:
for installed_file in installed_files:
paths_to_remove.add(os.path.join(dist_location, installed_file))
# FIXME: need a test for this elif block
# occurs with --single-version-externally-managed/--record outside
# of pip
elif dist.has_metadata('top_level.txt'):
if dist.has_metadata('namespace_packages.txt'):
namespaces = dist.get_metadata('namespace_packages.txt')
else:
elif dist.is_file("top_level.txt"):
try:
namespace_packages = dist.read_text("namespace_packages.txt")
except FileNotFoundError:
namespaces = []
else:
namespaces = namespace_packages.splitlines(keepends=False)
for top_level_pkg in [
p for p
in dist.get_metadata('top_level.txt').splitlines()
if p and p not in namespaces]:
path = os.path.join(dist.location, top_level_pkg)
p
for p in dist.read_text("top_level.txt").splitlines()
if p and p not in namespaces
]:
path = os.path.join(dist_location, top_level_pkg)
paths_to_remove.add(path)
paths_to_remove.add(path + '.py')
paths_to_remove.add(path + '.pyc')
paths_to_remove.add(path + '.pyo')
paths_to_remove.add(f"{path}.py")
paths_to_remove.add(f"{path}.pyc")
paths_to_remove.add(f"{path}.pyo")
elif distutils_egg_info:
elif dist.installed_by_distutils:
raise UninstallationError(
"Cannot uninstall {!r}. It is a distutils installed project "
"and thus we cannot accurately determine which files belong "
"to it which would lead to only a partial uninstall.".format(
dist.project_name,
dist.raw_name,
)
)
elif dist.location.endswith('.egg'):
elif dist.installed_as_egg:
# package installed by easy_install
# We cannot match on dist.egg_name because it can slightly vary
# i.e. setuptools-0.6c11-py2.6.egg vs setuptools-0.6rc11-py2.6.egg
paths_to_remove.add(dist.location)
easy_install_egg = os.path.split(dist.location)[1]
easy_install_pth = os.path.join(os.path.dirname(dist.location),
'easy-install.pth')
paths_to_remove.add_pth(easy_install_pth, './' + easy_install_egg)
paths_to_remove.add(dist_location)
easy_install_egg = os.path.split(dist_location)[1]
easy_install_pth = os.path.join(
os.path.dirname(dist_location),
"easy-install.pth",
)
paths_to_remove.add_pth(easy_install_pth, "./" + easy_install_egg)
elif egg_info_exists and dist.egg_info.endswith('.dist-info'):
elif dist.installed_with_dist_info:
for path in uninstallation_paths(dist):
paths_to_remove.add(path)
elif develop_egg_link:
# develop egg
# PEP 660 modern editable is handled in the ``.dist-info`` case
# above, so this only covers the setuptools-style editable.
with open(develop_egg_link) as fh:
link_pointer = os.path.normcase(fh.readline().strip())
assert (link_pointer == dist.location), (
'Egg-link {} does not match installed location of {} '
'(at {})'.format(
link_pointer, dist.project_name, dist.location)
assert link_pointer == dist_location, (
f"Egg-link {link_pointer} does not match installed location of "
f"{dist.raw_name} (at {dist_location})"
)
paths_to_remove.add(develop_egg_link)
easy_install_pth = os.path.join(os.path.dirname(develop_egg_link),
'easy-install.pth')
paths_to_remove.add_pth(easy_install_pth, dist.location)
easy_install_pth = os.path.join(
os.path.dirname(develop_egg_link), "easy-install.pth"
)
paths_to_remove.add_pth(easy_install_pth, dist_location)
else:
logger.debug(
'Not sure how to uninstall: %s - Check: %s',
dist, dist.location,
"Not sure how to uninstall: %s - Check: %s",
dist,
dist_location,
)
if dist.in_usersite:
bin_dir = get_bin_user()
else:
bin_dir = get_bin_prefix()
# find distutils scripts= scripts
if dist.has_metadata('scripts') and dist.metadata_isdir('scripts'):
for script in dist.metadata_listdir('scripts'):
if dist_in_usersite(dist):
bin_dir = get_bin_user()
else:
bin_dir = get_bin_prefix()
paths_to_remove.add(os.path.join(bin_dir, script))
try:
for script in dist.iterdir("scripts"):
paths_to_remove.add(os.path.join(bin_dir, script.name))
if WINDOWS:
paths_to_remove.add(os.path.join(bin_dir, script) + '.bat')
paths_to_remove.add(os.path.join(bin_dir, f"{script.name}.bat"))
except (FileNotFoundError, NotADirectoryError):
pass
# find console_scripts
_scripts_to_remove = []
console_scripts = dist.get_entry_map(group='console_scripts')
for name in console_scripts.keys():
_scripts_to_remove.extend(_script_names(dist, name, False))
# find gui_scripts
gui_scripts = dist.get_entry_map(group='gui_scripts')
for name in gui_scripts.keys():
_scripts_to_remove.extend(_script_names(dist, name, True))
# find console_scripts and gui_scripts
def iter_scripts_to_remove(
dist: BaseDistribution,
bin_dir: str,
) -> Iterator[str]:
for entry_point in dist.iter_entry_points():
if entry_point.group == "console_scripts":
yield from _script_names(bin_dir, entry_point.name, False)
elif entry_point.group == "gui_scripts":
yield from _script_names(bin_dir, entry_point.name, True)
for s in _scripts_to_remove:
for s in iter_scripts_to_remove(dist, bin_dir):
paths_to_remove.add(s)
return paths_to_remove
@@ -585,45 +593,41 @@ class UninstallPthEntries:
# have more than "\\sever\share". Valid examples: "\\server\share\" or
# "\\server\share\folder".
if WINDOWS and not os.path.splitdrive(entry)[0]:
entry = entry.replace('\\', '/')
entry = entry.replace("\\", "/")
self.entries.add(entry)
def remove(self) -> None:
logger.verbose('Removing pth entries from %s:', self.file)
logger.verbose("Removing pth entries from %s:", self.file)
# If the file doesn't exist, log a warning and return
if not os.path.isfile(self.file):
logger.warning(
"Cannot remove entries from nonexistent file %s", self.file
)
logger.warning("Cannot remove entries from nonexistent file %s", self.file)
return
with open(self.file, 'rb') as fh:
with open(self.file, "rb") as fh:
# windows uses '\r\n' with py3k, but uses '\n' with py2.x
lines = fh.readlines()
self._saved_lines = lines
if any(b'\r\n' in line for line in lines):
endline = '\r\n'
if any(b"\r\n" in line for line in lines):
endline = "\r\n"
else:
endline = '\n'
endline = "\n"
# handle missing trailing newline
if lines and not lines[-1].endswith(endline.encode("utf-8")):
lines[-1] = lines[-1] + endline.encode("utf-8")
for entry in self.entries:
try:
logger.verbose('Removing entry: %s', entry)
logger.verbose("Removing entry: %s", entry)
lines.remove((entry + endline).encode("utf-8"))
except ValueError:
pass
with open(self.file, 'wb') as fh:
with open(self.file, "wb") as fh:
fh.writelines(lines)
def rollback(self) -> bool:
if self._saved_lines is None:
logger.error(
'Cannot roll back changes to %s, none were made', self.file
)
logger.error("Cannot roll back changes to %s, none were made", self.file)
return False
logger.debug('Rolling %s back to previous state', self.file)
with open(self.file, 'wb') as fh:
logger.debug("Rolling %s back to previous state", self.file)
with open(self.file, "wb") as fh:
fh.writelines(self._saved_lines)
return True
@@ -1,9 +1,11 @@
from typing import Callable, List
from typing import Callable, List, Optional
from pipenv.patched.notpip._internal.req.req_install import InstallRequirement
from pipenv.patched.notpip._internal.req.req_set import RequirementSet
InstallRequirementProvider = Callable[[str, InstallRequirement], InstallRequirement]
InstallRequirementProvider = Callable[
[str, Optional[InstallRequirement]], InstallRequirement
]
class BaseResolver:
@@ -20,7 +20,7 @@ from itertools import chain
from typing import DefaultDict, Iterable, List, Optional, Set, Tuple
from pipenv.patched.notpip._vendor.packaging import specifiers
from pipenv.patched.notpip._vendor.pkg_resources import Distribution
from pipenv.patched.notpip._vendor.packaging.requirements import Requirement
from pipenv.patched.notpip._internal.cache import WheelCache
from pipenv.patched.notpip._internal.exceptions import (
@@ -28,9 +28,11 @@ from pipenv.patched.notpip._internal.exceptions import (
DistributionNotFound,
HashError,
HashErrors,
NoneMetadataError,
UnsupportedPythonVersion,
)
from pipenv.patched.notpip._internal.index.package_finder import PackageFinder
from pipenv.patched.notpip._internal.metadata import BaseDistribution
from pipenv.patched.notpip._internal.models.link import Link
from pipenv.patched.notpip._internal.operations.prepare import RequirementPreparer
from pipenv.patched.notpip._internal.req.req_install import (
@@ -41,8 +43,8 @@ from pipenv.patched.notpip._internal.req.req_set import RequirementSet
from pipenv.patched.notpip._internal.resolution.base import BaseResolver, InstallRequirementProvider
from pipenv.patched.notpip._internal.utils.compatibility_tags import get_supported
from pipenv.patched.notpip._internal.utils.logging import indent_log
from pipenv.patched.notpip._internal.utils.misc import dist_in_usersite, normalize_version_info
from pipenv.patched.notpip._internal.utils.packaging import check_requires_python, get_requires_python
from pipenv.patched.notpip._internal.utils.misc import normalize_version_info
from pipenv.patched.notpip._internal.utils.packaging import check_requires_python
logger = logging.getLogger(__name__)
@@ -50,7 +52,7 @@ DiscoveredDependencies = DefaultDict[str, List[InstallRequirement]]
def _check_dist_requires_python(
dist: Distribution,
dist: BaseDistribution,
version_info: Tuple[int, int, int],
ignore_requires_python: bool = False,
) -> None:
@@ -66,14 +68,21 @@ def _check_dist_requires_python(
:raises UnsupportedPythonVersion: When the given Python version isn't
compatible.
"""
requires_python = get_requires_python(dist)
# This idiosyncratically converts the SpecifierSet to str and let
# check_requires_python then parse it again into SpecifierSet. But this
# is the legacy resolver so I'm just not going to bother refactoring.
try:
requires_python = str(dist.requires_python)
except FileNotFoundError as e:
raise NoneMetadataError(dist, str(e))
try:
is_compatible = check_requires_python(
requires_python, version_info=version_info
requires_python,
version_info=version_info,
)
except specifiers.InvalidSpecifier as exc:
logger.warning(
"Package %r has an invalid Requires-Python: %s", dist.project_name, exc
"Package %r has an invalid Requires-Python: %s", dist.raw_name, exc
)
return
@@ -84,7 +93,7 @@ def _check_dist_requires_python(
if ignore_requires_python:
logger.debug(
"Ignoring failed Requires-Python check for package %r: %s not in %r",
dist.project_name,
dist.raw_name,
version,
requires_python,
)
@@ -92,7 +101,7 @@ def _check_dist_requires_python(
raise UnsupportedPythonVersion(
"Package {!r} requires a different Python: {} not in {!r}".format(
dist.project_name, version, requires_python
dist.raw_name, version, requires_python
)
)
@@ -194,7 +203,7 @@ class Resolver(BaseResolver):
"""
# Don't uninstall the conflict if doing a user install and the
# conflict is not a user install.
if not self.use_user_site or dist_in_usersite(req.satisfied_by):
if not self.use_user_site or req.satisfied_by.in_usersite:
req.should_reinstall = True
req.satisfied_by = None
@@ -303,7 +312,7 @@ class Resolver(BaseResolver):
req.original_link_is_in_wheel_cache = True
req.link = cache_entry.link
def _get_dist_for(self, req: InstallRequirement) -> Distribution:
def _get_dist_for(self, req: InstallRequirement) -> BaseDistribution:
"""Takes a InstallRequirement and returns a single AbstractDist \
representing a prepared variant of the same.
"""
@@ -378,11 +387,11 @@ class Resolver(BaseResolver):
more_reqs: List[InstallRequirement] = []
def add_req(subreq: Distribution, extras_requested: Iterable[str]) -> None:
sub_install_req = self._make_install_req(
str(subreq),
req_to_install,
)
def add_req(subreq: Requirement, extras_requested: Iterable[str]) -> None:
# This idiosyncratically converts the Requirement to str and let
# make_install_req then parse it again into Requirement. But this is
# the legacy resolver so I'm just not going to bother refactoring.
sub_install_req = self._make_install_req(str(subreq), req_to_install)
parent_req_name = req_to_install.name
to_scan_again, add_to_parent = requirement_set.add_requirement(
sub_install_req,
@@ -410,15 +419,20 @@ class Resolver(BaseResolver):
",".join(req_to_install.extras),
)
missing_requested = sorted(
set(req_to_install.extras) - set(dist.extras)
set(req_to_install.extras) - set(dist.iter_provided_extras())
)
for missing in missing_requested:
logger.warning("%s does not provide the extra '%s'", dist, missing)
logger.warning(
"%s %s does not provide the extra '%s'",
dist.raw_name,
dist.version,
missing,
)
available_requested = sorted(
set(dist.extras) & set(req_to_install.extras)
set(dist.iter_provided_extras()) & set(req_to_install.extras)
)
for subreq in dist.requires(available_requested):
for subreq in dist.iter_dependencies(available_requested):
add_req(subreq, extras_requested=available_requested)
return more_reqs
@@ -36,11 +36,8 @@ class Constraint:
links = frozenset([ireq.link]) if ireq.link else frozenset()
return Constraint(ireq.specifier, ireq.hashes(trust_internet=False), links)
def __nonzero__(self) -> bool:
return bool(self.specifier) or bool(self.hashes) or bool(self.links)
def __bool__(self) -> bool:
return self.__nonzero__()
return bool(self.specifier) or bool(self.hashes) or bool(self.links)
def __and__(self, other: InstallRequirement) -> "Constraint":
if not isinstance(other, InstallRequirement):
@@ -2,13 +2,16 @@ import logging
import sys
from typing import TYPE_CHECKING, Any, FrozenSet, Iterable, Optional, Tuple, Union, cast
from pipenv.patched.notpip._vendor.packaging.specifiers import InvalidSpecifier, SpecifierSet
from pipenv.patched.notpip._vendor.packaging.specifiers import SpecifierSet
from pipenv.patched.notpip._vendor.packaging.utils import NormalizedName, canonicalize_name
from pipenv.patched.notpip._vendor.packaging.version import Version
from pipenv.patched.notpip._vendor.packaging.version import parse as parse_version
from pipenv.patched.notpip._vendor.pkg_resources import Distribution
from pipenv.patched.notpip._internal.exceptions import HashError, MetadataInconsistent
from pipenv.patched.notpip._internal.exceptions import (
HashError,
InstallationSubprocessError,
MetadataInconsistent,
)
from pipenv.patched.notpip._internal.metadata import BaseDistribution
from pipenv.patched.notpip._internal.models.link import Link, links_equivalent
from pipenv.patched.notpip._internal.models.wheel import Wheel
from pipenv.patched.notpip._internal.req.constructors import (
@@ -16,8 +19,7 @@ from pipenv.patched.notpip._internal.req.constructors import (
install_req_from_line,
)
from pipenv.patched.notpip._internal.req.req_install import InstallRequirement
from pipenv.patched.notpip._internal.utils.misc import dist_is_editable, normalize_version_info
from pipenv.patched.notpip._internal.utils.packaging import get_requires_python
from pipenv.patched.notpip._internal.utils.misc import normalize_version_info
from .base import Candidate, CandidateVersion, Requirement, format_name
@@ -85,6 +87,7 @@ def make_install_req_from_editable(
use_pep517=template.use_pep517,
isolated=template.isolated,
constraint=template.constraint,
permit_editable_wheels=template.permit_editable_wheels,
options=dict(
install_options=template.install_options,
global_options=template.global_options,
@@ -93,16 +96,15 @@ def make_install_req_from_editable(
)
def make_install_req_from_dist(
dist: Distribution, template: InstallRequirement
def _make_install_req_from_dist(
dist: BaseDistribution, template: InstallRequirement
) -> InstallRequirement:
project_name = canonicalize_name(dist.project_name)
if template.req:
line = str(template.req)
elif template.link:
line = f"{project_name} @ {template.link.url}"
line = f"{dist.canonical_name} @ {template.link.url}"
else:
line = f"{project_name}=={dist.parsed_version}"
line = f"{dist.canonical_name}=={dist.version}"
ireq = install_req_from_line(
line,
user_supplied=template.user_supplied,
@@ -136,6 +138,7 @@ class _InstallRequirementBackedCandidate(Candidate):
found remote link (e.g. from pypi.org).
"""
dist: BaseDistribution
is_installed = False
def __init__(
@@ -180,7 +183,7 @@ class _InstallRequirementBackedCandidate(Candidate):
def project_name(self) -> NormalizedName:
"""The normalised name of the project the candidate refers to"""
if self._name is None:
self._name = canonicalize_name(self.dist.project_name)
self._name = self.dist.canonical_name
return self._name
@property
@@ -190,7 +193,7 @@ class _InstallRequirementBackedCandidate(Candidate):
@property
def version(self) -> CandidateVersion:
if self._version is None:
self._version = parse_version(self.dist.version)
self._version = self.dist.version
return self._version
def format_for_error(self) -> str:
@@ -200,29 +203,27 @@ class _InstallRequirementBackedCandidate(Candidate):
self._link.file_path if self._link.is_file else self._link,
)
def _prepare_distribution(self) -> Distribution:
def _prepare_distribution(self) -> BaseDistribution:
raise NotImplementedError("Override in subclass")
def _check_metadata_consistency(self, dist: Distribution) -> None:
def _check_metadata_consistency(self, dist: BaseDistribution) -> None:
"""Check for consistency of project name and version of dist."""
canonical_name = canonicalize_name(dist.project_name)
if self._name is not None and self._name != canonical_name:
if self._name is not None and self._name != dist.canonical_name:
raise MetadataInconsistent(
self._ireq,
"name",
self._name,
dist.project_name,
dist.canonical_name,
)
parsed_version = parse_version(dist.version)
if self._version is not None and self._version != parsed_version:
if self._version is not None and self._version != dist.version:
raise MetadataInconsistent(
self._ireq,
"version",
str(self._version),
dist.version,
str(dist.version),
)
def _prepare(self) -> Distribution:
def _prepare(self) -> BaseDistribution:
try:
dist = self._prepare_distribution()
except HashError as e:
@@ -231,26 +232,19 @@ class _InstallRequirementBackedCandidate(Candidate):
# offending line to the user.
e.req = self._ireq
raise
except InstallationSubprocessError as exc:
# The output has been presented already, so don't duplicate it.
exc.context = "See above for output."
raise
self._check_metadata_consistency(dist)
return dist
def _get_requires_python_dependency(self) -> Optional[Requirement]:
requires_python = get_requires_python(self.dist)
if requires_python is None:
return None
try:
spec = SpecifierSet(requires_python)
except InvalidSpecifier as e:
message = "Package %r has an invalid Requires-Python: %s"
logger.warning(message, self.name, e)
return None
return self._factory.make_requires_python_requirement(spec)
def iter_dependencies(self, with_requires: bool) -> Iterable[Optional[Requirement]]:
requires = self.dist.requires() if with_requires else ()
requires = self.dist.iter_dependencies() if with_requires else ()
for r in requires:
yield self._factory.make_requirement_from_spec(str(r), self._ireq)
yield self._get_requires_python_dependency()
yield self._factory.make_requires_python_requirement(self.dist.requires_python)
def get_install_requirement(self) -> Optional[InstallRequirement]:
ireq = self._ireq
@@ -304,10 +298,9 @@ class LinkCandidate(_InstallRequirementBackedCandidate):
version=version,
)
def _prepare_distribution(self) -> Distribution:
return self._factory.preparer.prepare_linked_requirement(
self._ireq, parallel_builds=True
)
def _prepare_distribution(self) -> BaseDistribution:
preparer = self._factory.preparer
return preparer.prepare_linked_requirement(self._ireq, parallel_builds=True)
class EditableCandidate(_InstallRequirementBackedCandidate):
@@ -330,7 +323,7 @@ class EditableCandidate(_InstallRequirementBackedCandidate):
version=version,
)
def _prepare_distribution(self) -> Distribution:
def _prepare_distribution(self) -> BaseDistribution:
return self._factory.preparer.prepare_editable_requirement(self._ireq)
@@ -340,17 +333,17 @@ class AlreadyInstalledCandidate(Candidate):
def __init__(
self,
dist: Distribution,
dist: BaseDistribution,
template: InstallRequirement,
factory: "Factory",
) -> None:
self.dist = dist
self._ireq = make_install_req_from_dist(dist, template)
self._ireq = _make_install_req_from_dist(dist, template)
self._factory = factory
# This is just logging some messages, so we can do it eagerly.
# The returned dist would be exactly the same as self.dist because we
# set satisfied_by in make_install_req_from_dist.
# set satisfied_by in _make_install_req_from_dist.
# TODO: Supply reason based on force_reinstall and upgrade_strategy.
skip_reason = "already satisfied"
factory.preparer.prepare_installed_requirement(self._ireq, skip_reason)
@@ -374,7 +367,7 @@ class AlreadyInstalledCandidate(Candidate):
@property
def project_name(self) -> NormalizedName:
return canonicalize_name(self.dist.project_name)
return self.dist.canonical_name
@property
def name(self) -> str:
@@ -382,11 +375,11 @@ class AlreadyInstalledCandidate(Candidate):
@property
def version(self) -> CandidateVersion:
return parse_version(self.dist.version)
return self.dist.version
@property
def is_editable(self) -> bool:
return dist_is_editable(self.dist)
return self.dist.editable
def format_for_error(self) -> str:
return f"{self.name} {self.version} (Installed)"
@@ -394,7 +387,7 @@ class AlreadyInstalledCandidate(Candidate):
def iter_dependencies(self, with_requires: bool) -> Iterable[Optional[Requirement]]:
if not with_requires:
return
for r in self.dist.requires():
for r in self.dist.iter_dependencies():
yield self._factory.make_requirement_from_spec(str(r), self._ireq)
def get_install_requirement(self) -> Optional[InstallRequirement]:
@@ -494,8 +487,8 @@ class ExtrasCandidate(Candidate):
# The user may have specified extras that the candidate doesn't
# support. We ignore any unsupported extras here.
valid_extras = self.extras.intersection(self.base.dist.extras)
invalid_extras = self.extras.difference(self.base.dist.extras)
valid_extras = self.extras.intersection(self.base.dist.iter_provided_extras())
invalid_extras = self.extras.difference(self.base.dist.iter_provided_extras())
for extra in sorted(invalid_extras):
logger.warning(
"%s %s does not provide the extra '%s'",
@@ -504,7 +497,7 @@ class ExtrasCandidate(Candidate):
extra,
)
for r in self.base.dist.requires(valid_extras):
for r in self.base.dist.iter_dependencies(valid_extras):
requirement = factory.make_requirement_from_spec(
str(r), self.base._ireq, valid_extras
)
@@ -19,7 +19,6 @@ from typing import (
)
from pipenv.patched.notpip._vendor.packaging.requirements import InvalidRequirement
from pipenv.patched.notpip._vendor.packaging.requirements import Requirement as PackagingRequirement
from pipenv.patched.notpip._vendor.packaging.specifiers import SpecifierSet
from pipenv.patched.notpip._vendor.packaging.utils import NormalizedName, canonicalize_name
from pipenv.patched.notpip._vendor.resolvelib import ResolutionImpossible
@@ -46,6 +45,7 @@ from pipenv.patched.notpip._internal.req.req_install import (
from pipenv.patched.notpip._internal.resolution.base import InstallRequirementProvider
from pipenv.patched.notpip._internal.utils.compatibility_tags import get_supported
from pipenv.patched.notpip._internal.utils.hashes import Hashes
from pipenv.patched.notpip._internal.utils.packaging import get_requirement
from pipenv.patched.notpip._internal.utils.virtualenv import running_under_virtualenv
from .base import Candidate, CandidateVersion, Constraint, Requirement
@@ -97,6 +97,7 @@ class Factory:
force_reinstall: bool,
ignore_installed: bool,
ignore_requires_python: bool,
suppress_build_failures: bool,
py_version_info: Optional[Tuple[int, ...]] = None,
) -> None:
self._finder = finder
@@ -107,6 +108,7 @@ class Factory:
self._use_user_site = use_user_site
self._force_reinstall = force_reinstall
self._ignore_requires_python = ignore_requires_python
self._suppress_build_failures = suppress_build_failures
self._build_failures: Cache[InstallationError] = {}
self._link_candidate_cache: Cache[LinkCandidate] = {}
@@ -158,10 +160,7 @@ class Factory:
try:
base = self._installed_candidate_cache[dist.canonical_name]
except KeyError:
from pipenv.patched.notpip._internal.metadata.pkg_resources import Distribution as _Dist
compat_dist = cast(_Dist, dist)._dist
base = AlreadyInstalledCandidate(compat_dist, template, factory=self)
base = AlreadyInstalledCandidate(dist, template, factory=self)
self._installed_candidate_cache[dist.canonical_name] = base
if not extras:
return base
@@ -193,10 +192,22 @@ class Factory:
name=name,
version=version,
)
except (InstallationSubprocessError, MetadataInconsistent) as e:
logger.warning("Discarding %s. %s", link, e)
except MetadataInconsistent as e:
logger.info(
"Discarding [blue underline]%s[/]: [yellow]%s[reset]",
link,
e,
extra={"markup": True},
)
self._build_failures[link] = e
return None
except InstallationSubprocessError as e:
if not self._suppress_build_failures:
raise
logger.warning("Discarding %s due to build failure: %s", link, e)
self._build_failures[link] = e
return None
base: BaseCandidate = self._editable_candidate_cache[link]
else:
if link not in self._link_candidate_cache:
@@ -208,8 +219,19 @@ class Factory:
name=name,
version=version,
)
except (InstallationSubprocessError, MetadataInconsistent) as e:
logger.warning("Discarding %s. %s", link, e)
except MetadataInconsistent as e:
logger.info(
"Discarding [blue underline]%s[/]: [yellow]%s[reset]",
link,
e,
extra={"markup": True},
)
self._build_failures[link] = e
return None
except InstallationSubprocessError as e:
if not self._suppress_build_failures:
raise
logger.warning("Discarding %s due to build failure: %s", link, e)
self._build_failures[link] = e
return None
base = self._link_candidate_cache[link]
@@ -263,7 +285,7 @@ class Factory:
extras=extras,
template=template,
)
# The candidate is a known incompatiblity. Don't use it.
# The candidate is a known incompatibility. Don't use it.
if id(candidate) in incompatible_ids:
return None
return candidate
@@ -276,14 +298,27 @@ class Factory:
)
icans = list(result.iter_applicable())
# PEP 592: Yanked releases must be ignored unless only yanked
# releases can satisfy the version range. So if this is false,
# all yanked icans need to be skipped.
# PEP 592: Yanked releases are ignored unless the specifier
# explicitly pins a version (via '==' or '===') that can be
# solely satisfied by a yanked release.
all_yanked = all(ican.link.is_yanked for ican in icans)
def is_pinned(specifier: SpecifierSet) -> bool:
for sp in specifier:
if sp.operator == "===":
return True
if sp.operator != "==":
continue
if sp.version.endswith(".*"):
continue
return True
return False
pinned = is_pinned(specifier)
# PackageFinder returns earlier versions first, so we reverse.
for ican in reversed(icans):
if not all_yanked and ican.link.is_yanked:
if not (all_yanked and pinned) and ican.link.is_yanked:
continue
func = functools.partial(
self._make_candidate_from_link,
@@ -350,7 +385,7 @@ class Factory:
def find_candidates(
self,
identifier: str,
requirements: Mapping[str, Iterator[Requirement]],
requirements: Mapping[str, Iterable[Requirement]],
incompatibilities: Mapping[str, Iterator[Candidate]],
constraint: Constraint,
prefers_installed: bool,
@@ -368,7 +403,7 @@ class Factory:
# If the current identifier contains extras, add explicit candidates
# from entries from extra-less identifier.
with contextlib.suppress(InvalidRequirement):
parsed_requirement = PackagingRequirement(identifier)
parsed_requirement = get_requirement(identifier)
explicit_candidates.update(
self._iter_explicit_candidates_from_base(
requirements.get(parsed_requirement.name, ()),
@@ -377,7 +412,7 @@ class Factory:
)
# Add explicit candidates from constraints. We only do this if there are
# kown ireqs, which represent requirements not already explicit. If
# known ireqs, which represent requirements not already explicit. If
# there are no ireqs, we're constraining already-explicit requirements,
# which is handled later when we return the explicit candidates.
if ireqs:
@@ -487,16 +522,20 @@ class Factory:
def make_requirement_from_spec(
self,
specifier: str,
comes_from: InstallRequirement,
comes_from: Optional[InstallRequirement],
requested_extras: Iterable[str] = (),
) -> Optional[Requirement]:
ireq = self._make_install_req_from_spec(specifier, comes_from)
return self._make_requirement_from_install_req(ireq, requested_extras)
def make_requires_python_requirement(
self, specifier: Optional[SpecifierSet]
self,
specifier: SpecifierSet,
) -> Optional[Requirement]:
if self._ignore_requires_python or specifier is None:
if self._ignore_requires_python:
return None
# Don't bother creating a dependency for an empty Requires-Python.
if not str(specifier):
return None
return RequiresPythonRequirement(specifier, self._python_candidate)
@@ -614,7 +653,7 @@ class Factory:
]
if requires_python_causes:
# The comprehension above makes sure all Requirement instances are
# RequiresPythonRequirement, so let's cast for convinience.
# RequiresPythonRequirement, so let's cast for convenience.
return self._report_requires_python_error(
cast("Sequence[ConflictCause]", requires_python_causes),
)
@@ -695,6 +734,6 @@ class Factory:
return DistributionNotFound(
"ResolutionImpossible: for help visit "
"https://pip.pypa.io/en/latest/user_guide/"
"#fixing-conflicting-dependencies"
"https://pip.pypa.io/en/latest/topics/dependency-resolution/"
"#dealing-with-dependency-conflicts"
)
@@ -9,15 +9,30 @@ something.
"""
import functools
from typing import Callable, Iterator, Optional, Set, Tuple
from collections.abc import Sequence
from typing import TYPE_CHECKING, Any, Callable, Iterator, Optional, Set, Tuple
from pipenv.patched.notpip._vendor.packaging.version import _BaseVersion
from pipenv.patched.notpip._vendor.six.moves import collections_abc # type: ignore
from .base import Candidate
IndexCandidateInfo = Tuple[_BaseVersion, Callable[[], Optional[Candidate]]]
if TYPE_CHECKING:
SequenceCandidate = Sequence[Candidate]
else:
# For compatibility: Python before 3.9 does not support using [] on the
# Sequence class.
#
# >>> from collections.abc import Sequence
# >>> Sequence[str]
# Traceback (most recent call last):
# File "<stdin>", line 1, in <module>
# TypeError: 'ABCMeta' object is not subscriptable
#
# TODO: Remove this block after dropping Python 3.8 support.
SequenceCandidate = Sequence
def _iter_built(infos: Iterator[IndexCandidateInfo]) -> Iterator[Candidate]:
"""Iterator for ``FoundCandidates``.
@@ -90,7 +105,7 @@ def _iter_built_with_inserted(
yield installed
class FoundCandidates(collections_abc.Sequence):
class FoundCandidates(SequenceCandidate):
"""A lazy sequence to provide candidates to the resolver.
The intended usage is to return this from `find_matches()` so the resolver
@@ -111,7 +126,7 @@ class FoundCandidates(collections_abc.Sequence):
self._prefers_installed = prefers_installed
self._incompatible_ids = incompatible_ids
def __getitem__(self, index: int) -> Candidate:
def __getitem__(self, index: Any) -> Any:
# Implemented to satisfy the ABC check. This is not needed by the
# resolver, and should not be used by the provider either (for
# performance reasons).
@@ -138,5 +153,3 @@ class FoundCandidates(collections_abc.Sequence):
if self._prefers_installed and self._installed:
return True
return any(self)
__nonzero__ = __bool__ # XXX: Python 2.
@@ -1,6 +1,15 @@
import collections
import math
from typing import TYPE_CHECKING, Dict, Iterable, Iterator, Mapping, Sequence, Union
from typing import (
TYPE_CHECKING,
Dict,
Iterable,
Iterator,
Mapping,
Sequence,
TypeVar,
Union,
)
from pipenv.patched.notpip._vendor.resolvelib.providers import AbstractProvider
@@ -37,6 +46,35 @@ else:
# services to those objects (access to pip's finder and preparer).
D = TypeVar("D")
V = TypeVar("V")
def _get_with_identifier(
mapping: Mapping[str, V],
identifier: str,
default: D,
) -> Union[D, V]:
"""Get item from a package name lookup mapping with a resolver identifier.
This extra logic is needed when the target mapping is keyed by package
name, which cannot be directly looked up with an identifier (which may
contain requested extras). Additional logic is added to also look up a value
by "cleaning up" the extras from the identifier.
"""
if identifier in mapping:
return mapping[identifier]
# HACK: Theoretically we should check whether this identifier is a valid
# "NAME[EXTRAS]" format, and parse out the name part with packaging or
# some regular expression. But since pip's resolver only spits out three
# kinds of identifiers: normalized PEP 503 names, normalized names plus
# extras, and Requires-Python, we can cheat a bit here.
name, open_bracket, _ = identifier.partition("[")
if open_bracket and name in mapping:
return mapping[name]
return default
class PipProvider(_ProviderBase):
"""Pip's provider implementation for resolvelib.
@@ -66,12 +104,13 @@ class PipProvider(_ProviderBase):
def identify(self, requirement_or_candidate: Union[Requirement, Candidate]) -> str:
return requirement_or_candidate.name
def get_preference(
def get_preference( # type: ignore
self,
identifier: str,
resolutions: Mapping[str, Candidate],
candidates: Mapping[str, Iterator[Candidate]],
information: Mapping[str, Iterator["PreferenceInformation"]],
information: Mapping[str, Iterable["PreferenceInformation"]],
backtrack_causes: Sequence["PreferenceInformation"],
) -> "Preference":
"""Produce a sort key for given requirement based on preference.
@@ -112,9 +151,9 @@ class PipProvider(_ProviderBase):
for _, parent in information[identifier]
)
inferred_depth = min(d for d in parent_depths) + 1.0
self._known_depths[identifier] = inferred_depth
else:
inferred_depth = 1.0
self._known_depths[identifier] = inferred_depth
requested_order = self._user_requested.get(identifier, math.inf)
@@ -128,43 +167,34 @@ class PipProvider(_ProviderBase):
# (Most projects specify it only to request for an installer feature,
# which does not work, but that's another topic.) Intentionally
# delaying Setuptools helps reduce branches the resolver has to check.
# This serves as a temporary fix for issues like "apache-airlfow[all]"
# This serves as a temporary fix for issues like "apache-airflow[all]"
# while we work on "proper" branch pruning techniques.
delay_this = identifier == "setuptools"
# Prefer the causes of backtracking on the assumption that the problem
# resolving the dependency tree is related to the failures that caused
# the backtracking
backtrack_cause = self.is_backtrack_cause(identifier, backtrack_causes)
return (
not requires_python,
delay_this,
not direct,
not pinned,
not backtrack_cause,
inferred_depth,
requested_order,
not unfree,
identifier,
)
def _get_constraint(self, identifier: str) -> Constraint:
if identifier in self._constraints:
return self._constraints[identifier]
# HACK: Theoratically we should check whether this identifier is a valid
# "NAME[EXTRAS]" format, and parse out the name part with packaging or
# some regular expression. But since pip's resolver only spits out
# three kinds of identifiers: normalized PEP 503 names, normalized names
# plus extras, and Requires-Python, we can cheat a bit here.
name, open_bracket, _ = identifier.partition("[")
if open_bracket and name in self._constraints:
return self._constraints[name]
return Constraint.empty()
def find_matches(
self,
identifier: str,
requirements: Mapping[str, Iterator[Requirement]],
incompatibilities: Mapping[str, Iterator[Candidate]],
) -> Iterable[Candidate]:
def _eligible_for_upgrade(name: str) -> bool:
def _eligible_for_upgrade(identifier: str) -> bool:
"""Are upgrades allowed for this project?
This checks the upgrade strategy, and whether the project was one
@@ -178,13 +208,23 @@ class PipProvider(_ProviderBase):
if self._upgrade_strategy == "eager":
return True
elif self._upgrade_strategy == "only-if-needed":
return name in self._user_requested
user_order = _get_with_identifier(
self._user_requested,
identifier,
default=None,
)
return user_order is not None
return False
constraint = _get_with_identifier(
self._constraints,
identifier,
default=Constraint.empty(),
)
return self._factory.find_candidates(
identifier=identifier,
requirements=requirements,
constraint=self._get_constraint(identifier),
constraint=constraint,
prefers_installed=(not _eligible_for_upgrade(identifier)),
incompatibilities=incompatibilities,
)
@@ -195,3 +235,14 @@ class PipProvider(_ProviderBase):
def get_dependencies(self, candidate: Candidate) -> Sequence[Requirement]:
with_requires = not self._ignore_dependencies
return [r for r in candidate.iter_dependencies(with_requires) if r is not None]
@staticmethod
def is_backtrack_cause(
identifier: str, backtrack_causes: Sequence["PreferenceInformation"]
) -> bool:
for backtrack_cause in backtrack_causes:
if identifier == backtrack_cause.requirement.name:
return True
if backtrack_cause.parent and identifier == backtrack_cause.parent.name:
return True
return False
@@ -27,9 +27,8 @@ class PipReporter(BaseReporter):
13: (
"This is taking longer than usual. You might need to provide "
"the dependency resolver with stricter constraints to reduce "
"runtime. If you want to abort this run, you can press "
"Ctrl + C to do so. To improve how pip performs, tell us what "
"happened here: https://pip.pypa.io/surveys/backtracking"
"runtime. See https://pip.pypa.io/warnings/backtracking for "
"guidance. If you want to abort this run, press Ctrl + C."
),
}
@@ -21,12 +21,12 @@ class ExplicitRequirement(Requirement):
@property
def project_name(self) -> NormalizedName:
# No need to canonicalise - the candidate did this
# No need to canonicalize - the candidate did this
return self.candidate.project_name
@property
def name(self) -> str:
# No need to canonicalise - the candidate did this
# No need to canonicalize - the candidate did this
return self.candidate.name
def format_for_error(self) -> str:
@@ -19,8 +19,6 @@ from pipenv.patched.notpip._internal.resolution.resolvelib.reporter import (
PipDebuggingReporter,
PipReporter,
)
from pipenv.patched.notpip._internal.utils.deprecation import deprecated
from pipenv.patched.notpip._internal.utils.filetypes import is_archive_file
from .base import Candidate, Requirement
from .factory import Factory
@@ -49,6 +47,7 @@ class Resolver(BaseResolver):
ignore_requires_python: bool,
force_reinstall: bool,
upgrade_strategy: str,
suppress_build_failures: bool,
py_version_info: Optional[Tuple[int, ...]] = None,
):
super().__init__()
@@ -63,6 +62,7 @@ class Resolver(BaseResolver):
force_reinstall=force_reinstall,
ignore_installed=ignore_installed,
ignore_requires_python=ignore_requires_python,
suppress_build_failures=suppress_build_failures,
py_version_info=py_version_info,
)
self.ignore_dependencies = ignore_dependencies
@@ -136,25 +136,6 @@ class Resolver(BaseResolver):
)
continue
looks_like_sdist = (
is_archive_file(candidate.source_link.file_path)
and candidate.source_link.ext != ".zip"
)
if looks_like_sdist:
# is a local sdist -- show a deprecation warning!
reason = (
"Source distribution is being reinstalled despite an "
"installed package having the same name and version as "
"the installed package."
)
replacement = "use --force-reinstall"
deprecated(
reason=reason,
replacement=replacement,
gone_in="21.3",
issue=8711,
)
# is a local sdist or path -- reinstall
ireq.should_reinstall = True
else:
@@ -192,17 +173,19 @@ class Resolver(BaseResolver):
get installed one-by-one.
The current implementation creates a topological ordering of the
dependency graph, while breaking any cycles in the graph at arbitrary
points. We make no guarantees about where the cycle would be broken,
other than they would be broken.
dependency graph, giving more weight to packages with less
or no dependencies, while breaking any cycles in the graph at
arbitrary points. We make no guarantees about where the cycle
would be broken, other than it *would* be broken.
"""
assert self._result is not None, "must call resolve() first"
if not req_set.requirements:
# Nothing is left to install, so we do not need an order.
return []
graph = self._result.graph
weights = get_topological_weights(
graph,
expected_node_count=len(self._result.mapping) + 1,
)
weights = get_topological_weights(graph, set(req_set.requirements.keys()))
sorted_items = sorted(
req_set.requirements.items(),
@@ -213,23 +196,32 @@ class Resolver(BaseResolver):
def get_topological_weights(
graph: "DirectedGraph[Optional[str]]", expected_node_count: int
graph: "DirectedGraph[Optional[str]]", requirement_keys: Set[str]
) -> Dict[Optional[str], int]:
"""Assign weights to each node based on how "deep" they are.
This implementation may change at any point in the future without prior
notice.
We take the length for the longest path to any node from root, ignoring any
paths that contain a single node twice (i.e. cycles). This is done through
a depth-first search through the graph, while keeping track of the path to
the node.
We first simplify the dependency graph by pruning any leaves and giving them
the highest weight: a package without any dependencies should be installed
first. This is done again and again in the same way, giving ever less weight
to the newly found leaves. The loop stops when no leaves are left: all
remaining packages have at least one dependency left in the graph.
Then we continue with the remaining graph, by taking the length for the
longest path to any node from root, ignoring any paths that contain a single
node twice (i.e. cycles). This is done through a depth-first search through
the graph, while keeping track of the path to the node.
Cycles in the graph result would result in node being revisited while also
being it's own path. In this case, take no action. This helps ensure we
being on its own path. In this case, take no action. This helps ensure we
don't get stuck in a cycle.
When assigning weight, the longer path (i.e. larger length) is preferred.
We are only interested in the weights of packages that are in the
requirement_keys.
"""
path: Set[Optional[str]] = set()
weights: Dict[Optional[str], int] = {}
@@ -245,15 +237,49 @@ def get_topological_weights(
visit(child)
path.remove(node)
if node not in requirement_keys:
return
last_known_parent_count = weights.get(node, 0)
weights[node] = max(last_known_parent_count, len(path))
# Simplify the graph, pruning leaves that have no dependencies.
# This is needed for large graphs (say over 200 packages) because the
# `visit` function is exponentially slower then, taking minutes.
# See https://github.com/pypa/pip/issues/10557
# We will loop until we explicitly break the loop.
while True:
leaves = set()
for key in graph:
if key is None:
continue
for _child in graph.iter_children(key):
# This means we have at least one child
break
else:
# No child.
leaves.add(key)
if not leaves:
# We are done simplifying.
break
# Calculate the weight for the leaves.
weight = len(graph) - 1
for leaf in leaves:
if leaf not in requirement_keys:
continue
weights[leaf] = weight
# Remove the leaves from the graph, making it simpler.
for leaf in leaves:
graph.remove(leaf)
# Visit the remaining graph.
# `None` is guaranteed to be the root node by resolvelib.
visit(None)
# Sanity checks
assert weights[None] == 0
assert len(weights) == expected_node_count
# Sanity check: all requirement keys should be in the weights,
# and no other keys should be in the weights.
difference = set(weights.keys()).difference(requirement_keys)
assert not difference, difference
return weights
@@ -23,17 +23,15 @@ SELFCHECK_DATE_FMT = "%Y-%m-%dT%H:%M:%SZ"
logger = logging.getLogger(__name__)
def _get_statefile_name(key):
# type: (str) -> str
def _get_statefile_name(key: str) -> str:
key_bytes = key.encode()
name = hashlib.sha224(key_bytes).hexdigest()
return name
class SelfCheckState:
def __init__(self, cache_dir):
# type: (str) -> None
self.state = {} # type: Dict[str, Any]
def __init__(self, cache_dir: str) -> None:
self.state: Dict[str, Any] = {}
self.statefile_path = None
# Try to load the existing state
@@ -50,12 +48,10 @@ class SelfCheckState:
pass
@property
def key(self):
# type: () -> str
def key(self) -> str:
return sys.prefix
def save(self, pypi_version, current_time):
# type: (str, datetime.datetime) -> None
def save(self, pypi_version: str, current_time: datetime.datetime) -> None:
# If we do not have a path to cache in, don't bother saving.
if not self.statefile_path:
return
@@ -90,8 +86,7 @@ class SelfCheckState:
pass
def was_installed_by_pip(pkg):
# type: (str) -> bool
def was_installed_by_pip(pkg: str) -> bool:
"""Checks whether pkg was installed by pip
This is used not to display the upgrade message when pip is in fact
@@ -101,8 +96,7 @@ def was_installed_by_pip(pkg):
return dist is not None and "pip" == dist.installer
def pip_self_version_check(session, options):
# type: (PipSession, optparse.Values) -> None
def pip_self_version_check(session: PipSession, options: optparse.Values) -> None:
"""Check for an update for pip.
Limit the frequency of checks to once per week. State is stored either in
@@ -123,8 +117,7 @@ def pip_self_version_check(session, options):
# Determine if we need to refresh the state
if "last_check" in state.state and "pypi_version" in state.state:
last_check = datetime.datetime.strptime(
state.state["last_check"],
SELFCHECK_DATE_FMT
state.state["last_check"], SELFCHECK_DATE_FMT
)
if (current_time - last_check).total_seconds() < 7 * 24 * 60 * 60:
pypi_version = state.state["pypi_version"]
@@ -148,6 +141,9 @@ def pip_self_version_check(session, options):
finder = PackageFinder.create(
link_collector=link_collector,
selection_prefs=selection_prefs,
use_deprecated_html5lib=(
"html5lib" in options.deprecated_features_enabled
),
)
best_candidate = finder.find_best_candidate("pip").best_candidate
if best_candidate is None:
@@ -160,9 +156,9 @@ def pip_self_version_check(session, options):
remote_version = parse_version(pypi_version)
local_version_is_older = (
pip_version < remote_version and
pip_version.base_version != remote_version.base_version and
was_installed_by_pip('pip')
pip_version < remote_version
and pip_version.base_version != remote_version.base_version
and was_installed_by_pip("pip")
)
# Determine if our pypi_version is older
@@ -172,13 +168,19 @@ def pip_self_version_check(session, options):
# We cannot tell how the current pip is available in the current
# command context, so be pragmatic here and suggest the command
# that's always available. This does not accommodate spaces in
# `sys.executable`.
# `sys.executable` on purpose as it is not possible to do it
# correctly without knowing the user's shell. Thus,
# it won't be done until possible through the standard library.
# Do not be tempted to use the undocumented subprocess.list2cmdline.
# It is considered an internal implementation detail for a reason.
pip_cmd = f"{sys.executable} -m pip"
logger.warning(
"You are using pip version %s; however, version %s is "
"available.\nYou should consider upgrading via the "
"'%s install --upgrade pip' command.",
pip_version, pypi_version, pip_cmd
pip_version,
pypi_version,
pip_cmd,
)
except Exception:
logger.debug(
@@ -7,29 +7,46 @@ and eventually drop this after all usages are changed.
"""
import os
import sys
from typing import List
from pipenv.patched.notpip._vendor import appdirs as _appdirs
from pipenv.patched.notpip._vendor import platformdirs as _appdirs
def user_cache_dir(appname: str) -> str:
return _appdirs.user_cache_dir(appname, appauthor=False)
def _macos_user_config_dir(appname: str, roaming: bool = True) -> str:
# Use ~/Application Support/pip, if the directory exists.
path = _appdirs.user_data_dir(appname, appauthor=False, roaming=roaming)
if os.path.isdir(path):
return path
# Use a Linux-like ~/.config/pip, by default.
linux_like_path = "~/.config/"
if appname:
linux_like_path = os.path.join(linux_like_path, appname)
return os.path.expanduser(linux_like_path)
def user_config_dir(appname: str, roaming: bool = True) -> str:
path = _appdirs.user_config_dir(appname, appauthor=False, roaming=roaming)
if _appdirs.system == "darwin" and not os.path.isdir(path):
path = os.path.expanduser("~/.config/")
if appname:
path = os.path.join(path, appname)
return path
if sys.platform == "darwin":
return _macos_user_config_dir(appname, roaming)
return _appdirs.user_config_dir(appname, appauthor=False, roaming=roaming)
# for the discussion regarding site_config_dir locations
# see <https://github.com/pypa/pip/issues/1733>
def site_config_dirs(appname: str) -> List[str]:
if sys.platform == "darwin":
return [_appdirs.site_data_dir(appname, appauthor=False, multipath=True)]
dirval = _appdirs.site_config_dir(appname, appauthor=False, multipath=True)
if _appdirs.system not in ["win32", "darwin"]:
# always look in /etc directly as well
return dirval.split(os.pathsep) + ["/etc"]
return [dirval]
if sys.platform == "win32":
return [dirval]
# Unix-y system. Look in /etc as well.
return dirval.split(os.pathsep) + ["/etc"]
@@ -2,9 +2,10 @@
"""
import re
from typing import TYPE_CHECKING, List, Optional, Tuple
from typing import List, Optional, Tuple
from pipenv.patched.notpip._vendor.packaging.tags import (
PythonVersion,
Tag,
compatible_tags,
cpython_tags,
@@ -14,10 +15,6 @@ from pipenv.patched.notpip._vendor.packaging.tags import (
mac_platforms,
)
if TYPE_CHECKING:
from pipenv.patched.notpip._vendor.packaging.tags import PythonVersion
_osx_arch_pat = re.compile(r"(.+)_(\d+)_(\d+)_(.+)")
@@ -95,7 +92,7 @@ def _expand_allowed_platforms(platforms: Optional[List[str]]) -> Optional[List[s
return result
def _get_python_version(version: str) -> "PythonVersion":
def _get_python_version(version: str) -> PythonVersion:
if len(version) > 1:
return int(version[0]), int(version[1:])
else:
@@ -132,7 +129,7 @@ def get_supported(
"""
supported: List[Tag] = []
python_version: Optional["PythonVersion"] = None
python_version: Optional[PythonVersion] = None
if version is not None:
python_version = _get_python_version(version)
@@ -8,7 +8,7 @@ from typing import Any, Optional, TextIO, Type, Union
from pipenv.patched.notpip._vendor.packaging.version import parse
from pipenv.patched.notpip import __version__ as current_version
from pipenv.patched.notpip import __version__ as current_version # NOTE: tests patch this name.
DEPRECATION_MSG_PREFIX = "DEPRECATION: "
@@ -53,52 +53,68 @@ def install_warning_logger() -> None:
def deprecated(
*,
reason: str,
replacement: Optional[str],
gone_in: Optional[str],
feature_flag: Optional[str] = None,
issue: Optional[int] = None,
) -> None:
"""Helper to deprecate existing functionality.
reason:
Textual reason shown to the user about why this functionality has
been deprecated.
been deprecated. Should be a complete sentence.
replacement:
Textual suggestion shown to the user about what alternative
functionality they can use.
gone_in:
The version of pip does this functionality should get removed in.
Raises errors if pip's current version is greater than or equal to
Raises an error if pip's current version is greater than or equal to
this.
feature_flag:
Command-line flag of the form --use-feature={feature_flag} for testing
upcoming functionality.
issue:
Issue number on the tracker that would serve as a useful place for
users to find related discussion and provide feedback.
Always pass replacement, gone_in and issue as keyword arguments for clarity
at the call site.
"""
# Construct a nice message.
# This is eagerly formatted as we want it to get logged as if someone
# typed this entire message out.
sentences = [
(reason, DEPRECATION_MSG_PREFIX + "{}"),
(gone_in, "pip {} will remove support for this functionality."),
(replacement, "A possible replacement is {}."),
# Determine whether or not the feature is already gone in this version.
is_gone = gone_in is not None and parse(current_version) >= parse(gone_in)
message_parts = [
(reason, f"{DEPRECATION_MSG_PREFIX}{{}}"),
(
gone_in,
"pip {} will enforce this behaviour change."
if not is_gone
else "Since pip {}, this is no longer supported.",
),
(
replacement,
"A possible replacement is {}.",
),
(
feature_flag,
"You can use the flag --use-feature={} to test the upcoming behaviour."
if not is_gone
else None,
),
(
issue,
(
"You can find discussion regarding this at "
"https://github.com/pypa/pip/issues/{}."
),
"Discussion can be found at https://github.com/pypa/pip/issues/{}",
),
]
message = " ".join(
template.format(val) for val, template in sentences if val is not None
format_str.format(value)
for value, format_str in message_parts
if format_str is not None and value is not None
)
# Raise as an error if it has to be removed.
if gone_in is not None and parse(current_version) >= parse(gone_in):
# Raise as an error if this behaviour is deprecated.
if is_gone:
raise PipDeprecationWarning(message)
warnings.warn(message, category=PipDeprecationWarning, stacklevel=2)
@@ -2,6 +2,7 @@ from typing import Optional
from pipenv.patched.notpip._internal.models.direct_url import ArchiveInfo, DirectUrl, DirInfo, VcsInfo
from pipenv.patched.notpip._internal.models.link import Link
from pipenv.patched.notpip._internal.utils.urls import path_to_url
from pipenv.patched.notpip._internal.vcs import vcs
@@ -28,6 +29,13 @@ def direct_url_as_pep440_direct_reference(direct_url: DirectUrl, name: str) -> s
return requirement
def direct_url_for_editable(source_dir: str) -> DirectUrl:
return DirectUrl(
url=path_to_url(source_dir),
info=DirInfo(editable=True),
)
def direct_url_from_link(
link: Link, source_dir: Optional[str] = None, link_is_in_wheel_cache: bool = False
) -> DirectUrl:
@@ -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 pipenv.patched.notpip._internal.locations import site_packages, user_site
from pipenv.patched.notpip._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
@@ -6,21 +6,20 @@ from typing import Tuple
from pipenv.patched.notpip._internal.utils.misc import splitext
WHEEL_EXTENSION = ".whl"
BZ2_EXTENSIONS = (".tar.bz2", ".tbz") # type: Tuple[str, ...]
XZ_EXTENSIONS = (
BZ2_EXTENSIONS: Tuple[str, ...] = (".tar.bz2", ".tbz")
XZ_EXTENSIONS: Tuple[str, ...] = (
".tar.xz",
".txz",
".tlz",
".tar.lz",
".tar.lzma",
) # type: Tuple[str, ...]
ZIP_EXTENSIONS = (".zip", WHEEL_EXTENSION) # type: Tuple[str, ...]
TAR_EXTENSIONS = (".tar.gz", ".tgz", ".tar") # type: Tuple[str, ...]
)
ZIP_EXTENSIONS: Tuple[str, ...] = (".zip", WHEEL_EXTENSION)
TAR_EXTENSIONS: Tuple[str, ...] = (".tar.gz", ".tgz", ".tar")
ARCHIVE_EXTENSIONS = ZIP_EXTENSIONS + BZ2_EXTENSIONS + TAR_EXTENSIONS + XZ_EXTENSIONS
def is_archive_file(name):
# type: (str) -> bool
def is_archive_file(name: str) -> bool:
"""Return True if `name` is a considered as an archive file."""
ext = splitext(name)[1].lower()
if ext in ARCHIVE_EXTENSIONS:
@@ -6,14 +6,12 @@ import sys
from typing import Optional, Tuple
def glibc_version_string():
# type: () -> Optional[str]
def glibc_version_string() -> Optional[str]:
"Returns glibc version string, or None if not using glibc."
return glibc_version_string_confstr() or glibc_version_string_ctypes()
def glibc_version_string_confstr():
# type: () -> Optional[str]
def glibc_version_string_confstr() -> Optional[str]:
"Primary implementation of glibc_version_string using os.confstr."
# os.confstr is quite a bit faster than ctypes.DLL. It's also less likely
# to be broken or missing. This strategy is used in the standard library
@@ -30,8 +28,7 @@ def glibc_version_string_confstr():
return version
def glibc_version_string_ctypes():
# type: () -> Optional[str]
def glibc_version_string_ctypes() -> Optional[str]:
"Fallback implementation of glibc_version_string using ctypes."
try:
@@ -78,8 +75,7 @@ def glibc_version_string_ctypes():
# versions that was generated by pip 8.1.2 and earlier is useless and
# misleading. Solution: instead of using platform, use our code that actually
# works.
def libc_ver():
# type: () -> Tuple[str, str]
def libc_ver() -> Tuple[str, str]:
"""Try to determine the glibc version
Returns a tuple of strings (lib, version) which default to empty strings
+13 -34
View File
@@ -28,8 +28,7 @@ class Hashes:
"""
def __init__(self, hashes=None):
# type: (Dict[str, List[str]]) -> None
def __init__(self, hashes: Dict[str, List[str]] = None) -> None:
"""
:param hashes: A dict of algorithm names pointing to lists of allowed
hex digests
@@ -41,8 +40,7 @@ class Hashes:
allowed[alg] = sorted(keys)
self._allowed = allowed
def __and__(self, other):
# type: (Hashes) -> Hashes
def __and__(self, other: "Hashes") -> "Hashes":
if not isinstance(other, Hashes):
return NotImplemented
@@ -62,21 +60,14 @@ class Hashes:
return Hashes(new)
@property
def digest_count(self):
# type: () -> int
def digest_count(self) -> int:
return sum(len(digests) for digests in self._allowed.values())
def is_hash_allowed(
self,
hash_name, # type: str
hex_digest, # type: str
):
# type: (...) -> bool
def is_hash_allowed(self, hash_name: str, hex_digest: str) -> bool:
"""Return whether the given hex digest is allowed."""
return hex_digest in self._allowed.get(hash_name, [])
def check_against_chunks(self, chunks):
# type: (Iterator[bytes]) -> None
def check_against_chunks(self, chunks: Iterator[bytes]) -> None:
"""Check good hashes against ones built from iterable of chunks of
data.
@@ -99,12 +90,10 @@ class Hashes:
return
self._raise(gots)
def _raise(self, gots):
# type: (Dict[str, _Hash]) -> NoReturn
def _raise(self, gots: Dict[str, "_Hash"]) -> "NoReturn":
raise HashMismatch(self._allowed, gots)
def check_against_file(self, file):
# type: (BinaryIO) -> None
def check_against_file(self, file: BinaryIO) -> None:
"""Check good hashes against a file-like object
Raise HashMismatch if none match.
@@ -112,28 +101,20 @@ class Hashes:
"""
return self.check_against_chunks(read_chunks(file))
def check_against_path(self, path):
# type: (str) -> None
def check_against_path(self, path: str) -> None:
with open(path, "rb") as file:
return self.check_against_file(file)
def __nonzero__(self):
# type: () -> bool
def __bool__(self) -> bool:
"""Return whether I know any known-good hashes."""
return bool(self._allowed)
def __bool__(self):
# type: () -> bool
return self.__nonzero__()
def __eq__(self, other):
# type: (object) -> bool
def __eq__(self, other: object) -> bool:
if not isinstance(other, Hashes):
return NotImplemented
return self._allowed == other._allowed
def __hash__(self):
# type: () -> int
def __hash__(self) -> int:
return hash(
",".join(
sorted(
@@ -153,13 +134,11 @@ class MissingHashes(Hashes):
"""
def __init__(self):
# type: () -> None
def __init__(self) -> None:
"""Don't offer the ``hashes`` kwarg."""
# Pass our favorite hash in to generate a "gotten hash". With the
# empty list, it will never match, so an error will always raise.
super().__init__(hashes={FAVORITE_HASH: []})
def _raise(self, gots):
# type: (Dict[str, _Hash]) -> NoReturn
def _raise(self, gots: Dict[str, "_Hash"]) -> "NoReturn":
raise HashMissing(gots[FAVORITE_HASH].hexdigest())
@@ -10,8 +10,7 @@ old to handle TLSv1.2.
import sys
def inject_securetransport():
# type: () -> None
def inject_securetransport() -> None:
# Only relevant on macOS
if sys.platform != "darwin":
return
+84 -132
View File
@@ -4,28 +4,28 @@ import logging
import logging.handlers
import os
import sys
import threading
from dataclasses import dataclass
from logging import Filter
from typing import IO, Any, Callable, Iterator, Optional, TextIO, Type, cast
from typing import IO, Any, ClassVar, Iterator, List, Optional, TextIO, Type
from pipenv.patched.notpip._vendor.rich.console import (
Console,
ConsoleOptions,
ConsoleRenderable,
RenderResult,
)
from pipenv.patched.notpip._vendor.rich.highlighter import NullHighlighter
from pipenv.patched.notpip._vendor.rich.logging import RichHandler
from pipenv.patched.notpip._vendor.rich.segment import Segment
from pipenv.patched.notpip._vendor.rich.style import Style
from pipenv.patched.notpip._internal.exceptions import DiagnosticPipError
from pipenv.patched.notpip._internal.utils._log import VERBOSE, getLogger
from pipenv.patched.notpip._internal.utils.compat import WINDOWS
from pipenv.patched.notpip._internal.utils.deprecation import DEPRECATION_MSG_PREFIX
from pipenv.patched.notpip._internal.utils.misc import ensure_dir
try:
import threading
except ImportError:
import dummy_threading as threading # type: ignore
try:
from pipenv.patched.notpip._vendor import colorama
# Lots of different errors can come from this, including SystemError and
# ImportError.
except Exception:
colorama = None
_log_state = threading.local()
subprocess_logger = getLogger("pip.subprocessor")
@@ -35,39 +35,22 @@ class BrokenStdoutLoggingError(Exception):
Raised if BrokenPipeError occurs for the stdout stream while logging.
"""
pass
def _is_broken_pipe_error(exc_class: Type[BaseException], exc: BaseException) -> bool:
if exc_class is BrokenPipeError:
return True
# BrokenPipeError manifests differently in Windows and non-Windows.
if WINDOWS:
# In Windows, a broken pipe can show up as EINVAL rather than EPIPE:
# On Windows, a broken pipe can show up as EINVAL rather than EPIPE:
# https://bugs.python.org/issue19612
# https://bugs.python.org/issue30418
def _is_broken_pipe_error(exc_class, exc):
# type: (Type[BaseException], BaseException) -> bool
"""See the docstring for non-Windows below."""
return (exc_class is BrokenPipeError) or (
isinstance(exc, OSError) and exc.errno in (errno.EINVAL, errno.EPIPE)
)
if not WINDOWS:
return False
else:
# Then we are in the non-Windows case.
def _is_broken_pipe_error(exc_class, exc):
# type: (Type[BaseException], BaseException) -> bool
"""
Return whether an exception is a broken pipe error.
Args:
exc_class: an exception class.
exc: an exception instance.
"""
return exc_class is BrokenPipeError
return isinstance(exc, OSError) and exc.errno in (errno.EINVAL, errno.EPIPE)
@contextlib.contextmanager
def indent_log(num=2):
# type: (int) -> Iterator[None]
def indent_log(num: int = 2) -> Iterator[None]:
"""
A context manager which will cause the log output to be indented for any
log messages emitted inside it.
@@ -81,8 +64,7 @@ def indent_log(num=2):
_log_state.indentation -= num
def get_indentation():
# type: () -> int
def get_indentation() -> int:
return getattr(_log_state, "indentation", 0)
@@ -91,11 +73,10 @@ class IndentingFormatter(logging.Formatter):
def __init__(
self,
*args, # type: Any
add_timestamp=False, # type: bool
**kwargs, # type: Any
):
# type: (...) -> None
*args: Any,
add_timestamp: bool = False,
**kwargs: Any,
) -> None:
"""
A logging.Formatter that obeys the indent_log() context manager.
@@ -105,8 +86,7 @@ class IndentingFormatter(logging.Formatter):
self.add_timestamp = add_timestamp
super().__init__(*args, **kwargs)
def get_message_start(self, formatted, levelno):
# type: (str, int) -> str
def get_message_start(self, formatted: str, levelno: int) -> str:
"""
Return the start of the formatted log message (not counting the
prefix to add to each line).
@@ -122,8 +102,7 @@ class IndentingFormatter(logging.Formatter):
return "ERROR: "
def format(self, record):
# type: (logging.LogRecord) -> str
def format(self, record: logging.LogRecord) -> str:
"""
Calls the standard formatter, but will indent all of the log message
lines by our current indentation level.
@@ -140,85 +119,63 @@ class IndentingFormatter(logging.Formatter):
return formatted
def _color_wrap(*colors):
# type: (*str) -> Callable[[str], str]
def wrapped(inp):
# type: (str) -> str
return "".join(list(colors) + [inp, colorama.Style.RESET_ALL])
@dataclass
class IndentedRenderable:
renderable: ConsoleRenderable
indent: int
return wrapped
def __rich_console__(
self, console: Console, options: ConsoleOptions
) -> RenderResult:
segments = console.render(self.renderable, options)
lines = Segment.split_lines(segments)
for line in lines:
yield Segment(" " * self.indent)
yield from line
yield Segment("\n")
class ColorizedStreamHandler(logging.StreamHandler):
class RichPipStreamHandler(RichHandler):
KEYWORDS: ClassVar[Optional[List[str]]] = []
# Don't build up a list of colors if we don't have colorama
if colorama:
COLORS = [
# This needs to be in order from highest logging level to lowest.
(logging.ERROR, _color_wrap(colorama.Fore.RED)),
(logging.WARNING, _color_wrap(colorama.Fore.YELLOW)),
]
else:
COLORS = []
def __init__(self, stream=None, no_color=None):
# type: (Optional[TextIO], bool) -> None
super().__init__(stream)
self._no_color = no_color
if WINDOWS and colorama:
self.stream = colorama.AnsiToWin32(self.stream)
def _using_stdout(self):
# type: () -> bool
"""
Return whether the handler is using sys.stdout.
"""
if WINDOWS and colorama:
# Then self.stream is an AnsiToWin32 object.
stream = cast(colorama.AnsiToWin32, self.stream)
return stream.wrapped is sys.stdout
return self.stream is sys.stdout
def should_color(self):
# type: () -> bool
# Don't colorize things if we do not have colorama or if told not to
if not colorama or self._no_color:
return False
real_stream = (
self.stream
if not isinstance(self.stream, colorama.AnsiToWin32)
else self.stream.wrapped
def __init__(self, stream: Optional[TextIO], no_color: bool) -> None:
super().__init__(
console=Console(file=stream, no_color=no_color, soft_wrap=True),
show_time=False,
show_level=False,
show_path=False,
highlighter=NullHighlighter(),
)
# If the stream is a tty we should color it
if hasattr(real_stream, "isatty") and real_stream.isatty():
return True
# Our custom override on Rich's logger, to make things work as we need them to.
def emit(self, record: logging.LogRecord) -> None:
style: Optional[Style] = None
# If we have an ANSI term we should color it
if os.environ.get("TERM") == "ANSI":
return True
# If we are given a diagnostic error to present, present it with indentation.
if record.msg == "[present-diagnostic] %s" and len(record.args) == 1:
diagnostic_error: DiagnosticPipError = record.args[0] # type: ignore[index]
assert isinstance(diagnostic_error, DiagnosticPipError)
# If anything else we should not color it
return False
renderable: ConsoleRenderable = IndentedRenderable(
diagnostic_error, indent=get_indentation()
)
else:
message = self.format(record)
renderable = self.render_message(record, message)
if record.levelno is not None:
if record.levelno >= logging.ERROR:
style = Style(color="red")
elif record.levelno >= logging.WARNING:
style = Style(color="yellow")
def format(self, record):
# type: (logging.LogRecord) -> str
msg = super().format(record)
try:
self.console.print(renderable, overflow="ignore", crop=False, style=style)
except Exception:
self.handleError(record)
if self.should_color():
for level, color in self.COLORS:
if record.levelno >= level:
msg = color(msg)
break
def handleError(self, record: logging.LogRecord) -> None:
"""Called when logging is unable to log some output."""
return msg
# The logging module says handleError() can be customized.
def handleError(self, record):
# type: (logging.LogRecord) -> None
exc_class, exc = sys.exc_info()[:2]
# If a broken pipe occurred while calling write() or flush() on the
# stdout stream in logging's Handler.emit(), then raise our special
@@ -227,7 +184,7 @@ class ColorizedStreamHandler(logging.StreamHandler):
if (
exc_class
and exc
and self._using_stdout()
and self.console.file is sys.stdout
and _is_broken_pipe_error(exc_class, exc)
):
raise BrokenStdoutLoggingError()
@@ -236,19 +193,16 @@ class ColorizedStreamHandler(logging.StreamHandler):
class BetterRotatingFileHandler(logging.handlers.RotatingFileHandler):
def _open(self):
# type: () -> IO[Any]
def _open(self) -> IO[Any]:
ensure_dir(os.path.dirname(self.baseFilename))
return super()._open()
class MaxLevelFilter(Filter):
def __init__(self, level):
# type: (int) -> None
def __init__(self, level: int) -> None:
self.level = level
def filter(self, record):
# type: (logging.LogRecord) -> bool
def filter(self, record: logging.LogRecord) -> bool:
return record.levelno < self.level
@@ -258,15 +212,13 @@ class ExcludeLoggerFilter(Filter):
A logging Filter that excludes records from a logger (or its children).
"""
def filter(self, record):
# type: (logging.LogRecord) -> bool
def filter(self, record: logging.LogRecord) -> bool:
# The base Filter class allows only records from a logger (or its
# children).
return not super().filter(record)
def setup_logging(verbosity, no_color, user_log_file):
# type: (int, bool, Optional[str]) -> int
def setup_logging(verbosity: int, no_color: bool, user_log_file: Optional[str]) -> int:
"""Configures and sets up all of the logging
Returns the requested logging level, as its integer value.
@@ -308,7 +260,7 @@ def setup_logging(verbosity, no_color, user_log_file):
"stderr": "ext://sys.stderr",
}
handler_classes = {
"stream": "pipenv.patched.notpip._internal.utils.logging.ColorizedStreamHandler",
"stream": "pipenv.patched.notpip._internal.utils.logging.RichPipStreamHandler",
"file": "pipenv.patched.notpip._internal.utils.logging.BetterRotatingFileHandler",
}
handlers = ["console", "console_errors", "console_subprocess"] + (
@@ -366,8 +318,8 @@ def setup_logging(verbosity, no_color, user_log_file):
"console_subprocess": {
"level": level,
"class": handler_classes["stream"],
"no_color": no_color,
"stream": log_streams["stderr"],
"no_color": no_color,
"filters": ["restrict_to_subprocess"],
"formatter": "indent",
},
+59 -258
View File
@@ -18,10 +18,8 @@ from itertools import filterfalse, tee, zip_longest
from types import TracebackType
from typing import (
Any,
AnyStr,
BinaryIO,
Callable,
Container,
ContextManager,
Iterable,
Iterator,
@@ -34,17 +32,13 @@ from typing import (
cast,
)
from pipenv.patched.notpip._vendor.pkg_resources import Distribution
from pipenv.patched.notpip._vendor.tenacity import retry, stop_after_delay, wait_fixed
from pipenv.patched.notpip import __version__
from pipenv.patched.notpip._internal.exceptions import CommandError
from pipenv.patched.notpip._internal.locations import get_major_minor_version, site_packages, user_site
from pipenv.patched.notpip._internal.utils.compat import WINDOWS, stdlib_pkgs
from pipenv.patched.notpip._internal.utils.virtualenv import (
running_under_virtualenv,
virtualenv_no_global,
)
from pipenv.patched.notpip._internal.locations import get_major_minor_version
from pipenv.patched.notpip._internal.utils.compat import WINDOWS
from pipenv.patched.notpip._internal.utils.virtualenv import running_under_virtualenv
__all__ = [
"rmtree",
@@ -71,8 +65,7 @@ VersionInfo = Tuple[int, int, int]
NetlocTuple = Tuple[str, Tuple[Optional[str], Optional[str]]]
def get_pip_version():
# type: () -> str
def get_pip_version() -> str:
pip_pkg_dir = os.path.join(os.path.dirname(__file__), "..", "..")
pip_pkg_dir = os.path.abspath(pip_pkg_dir)
@@ -83,8 +76,7 @@ def get_pip_version():
)
def normalize_version_info(py_version_info):
# type: (Tuple[int, ...]) -> Tuple[int, int, int]
def normalize_version_info(py_version_info: Tuple[int, ...]) -> Tuple[int, int, int]:
"""
Convert a tuple of ints representing a Python version to one of length
three.
@@ -103,8 +95,7 @@ def normalize_version_info(py_version_info):
return cast("VersionInfo", py_version_info)
def ensure_dir(path):
# type: (AnyStr) -> None
def ensure_dir(path: str) -> None:
"""os.path.makedirs without EEXIST."""
try:
os.makedirs(path)
@@ -114,8 +105,7 @@ def ensure_dir(path):
raise
def get_prog():
# type: () -> str
def get_prog() -> str:
try:
prog = os.path.basename(sys.argv[0])
if prog in ("__main__.py", "-c"):
@@ -130,13 +120,11 @@ def get_prog():
# Retry every half second for up to 3 seconds
# Tenacity raises RetryError by default, explicitly raise the original exception
@retry(reraise=True, stop=stop_after_delay(3), wait=wait_fixed(0.5))
def rmtree(dir, ignore_errors=False):
# type: (AnyStr, bool) -> None
def rmtree(dir: str, ignore_errors: bool = False) -> None:
shutil.rmtree(dir, ignore_errors=ignore_errors, onerror=rmtree_errorhandler)
def rmtree_errorhandler(func, path, exc_info):
# type: (Callable[..., Any], str, ExcInfo) -> None
def rmtree_errorhandler(func: Callable[..., Any], path: str, exc_info: ExcInfo) -> None:
"""On Windows, the files in .svn are read-only, so when rmtree() tries to
remove them, an exception is thrown. We catch that here, remove the
read-only attribute, and hopefully continue without problems."""
@@ -156,8 +144,7 @@ def rmtree_errorhandler(func, path, exc_info):
raise
def display_path(path):
# type: (str) -> str
def display_path(path: str) -> str:
"""Gives the display value for a given path, making it relative to cwd
if possible."""
path = os.path.normcase(os.path.abspath(path))
@@ -166,8 +153,7 @@ def display_path(path):
return path
def backup_dir(dir, ext=".bak"):
# type: (str, str) -> str
def backup_dir(dir: str, ext: str = ".bak") -> str:
"""Figure out the name of a directory to back up the given dir to
(adding .bak, .bak2, etc)"""
n = 1
@@ -178,16 +164,14 @@ def backup_dir(dir, ext=".bak"):
return dir + extension
def ask_path_exists(message, options):
# type: (str, Iterable[str]) -> str
def ask_path_exists(message: str, options: Iterable[str]) -> str:
for action in os.environ.get("PIP_EXISTS_ACTION", "").split():
if action in options:
return action
return ask(message, options)
def _check_no_input(message):
# type: (str) -> None
def _check_no_input(message: str) -> None:
"""Raise an error if no input is allowed."""
if os.environ.get("PIP_NO_INPUT"):
raise Exception(
@@ -195,8 +179,7 @@ def _check_no_input(message):
)
def ask(message, options):
# type: (str, Iterable[str]) -> str
def ask(message: str, options: Iterable[str]) -> str:
"""Ask the message interactively, with the given possible responses"""
while 1:
_check_no_input(message)
@@ -211,22 +194,19 @@ def ask(message, options):
return response
def ask_input(message):
# type: (str) -> str
def ask_input(message: str) -> str:
"""Ask for input interactively."""
_check_no_input(message)
return input(message)
def ask_password(message):
# type: (str) -> str
def ask_password(message: str) -> str:
"""Ask for a password interactively."""
_check_no_input(message)
return getpass.getpass(message)
def strtobool(val):
# type: (str) -> int
def strtobool(val: str) -> int:
"""Convert a string representation of truth to true (1) or false (0).
True values are 'y', 'yes', 't', 'true', 'on', and '1'; false values
@@ -242,8 +222,7 @@ def strtobool(val):
raise ValueError(f"invalid truth value {val!r}")
def format_size(bytes):
# type: (float) -> str
def format_size(bytes: float) -> str:
if bytes > 1000 * 1000:
return "{:.1f} MB".format(bytes / 1000.0 / 1000)
elif bytes > 10 * 1000:
@@ -254,8 +233,7 @@ def format_size(bytes):
return "{} bytes".format(int(bytes))
def tabulate(rows):
# type: (Iterable[Iterable[Any]]) -> Tuple[List[str], List[int]]
def tabulate(rows: Iterable[Iterable[Any]]) -> Tuple[List[str], List[int]]:
"""Return a list of formatted rows and a list of column sizes.
For example::
@@ -286,8 +264,7 @@ def is_installable_dir(path: str) -> bool:
return False
def read_chunks(file, size=io.DEFAULT_BUFFER_SIZE):
# type: (BinaryIO, int) -> Iterator[bytes]
def read_chunks(file: BinaryIO, size: int = io.DEFAULT_BUFFER_SIZE) -> Iterator[bytes]:
"""Yield pieces of data from a file-like object until EOF."""
while True:
chunk = file.read(size)
@@ -296,8 +273,7 @@ def read_chunks(file, size=io.DEFAULT_BUFFER_SIZE):
yield chunk
def normalize_path(path, resolve_symlinks=True):
# type: (str, bool) -> str
def normalize_path(path: str, resolve_symlinks: bool = True) -> str:
"""
Convert a path to its canonical, case-normalized, absolute version.
@@ -310,8 +286,7 @@ def normalize_path(path, resolve_symlinks=True):
return os.path.normcase(path)
def splitext(path):
# type: (str) -> Tuple[str, str]
def splitext(path: str) -> Tuple[str, str]:
"""Like os.path.splitext, but take off .tar too"""
base, ext = posixpath.splitext(path)
if base.lower().endswith(".tar"):
@@ -320,8 +295,7 @@ def splitext(path):
return base, ext
def renames(old, new):
# type: (str, str) -> None
def renames(old: str, new: str) -> None:
"""Like os.renames(), but handles renaming across devices."""
# Implementation borrowed from os.renames().
head, tail = os.path.split(new)
@@ -338,8 +312,7 @@ def renames(old, new):
pass
def is_local(path):
# type: (str) -> bool
def is_local(path: str) -> bool:
"""
Return True if path is within sys.prefix, if we're running in a virtualenv.
@@ -353,158 +326,15 @@ def is_local(path):
return path.startswith(normalize_path(sys.prefix))
def dist_is_local(dist):
# type: (Distribution) -> bool
"""
Return True if given Distribution object is installed locally
(i.e. within current virtualenv).
Always True if we're not in a virtualenv.
"""
return is_local(dist_location(dist))
def dist_in_usersite(dist):
# type: (Distribution) -> bool
"""
Return True if given Distribution is installed in user site.
"""
return dist_location(dist).startswith(normalize_path(user_site))
def dist_in_site_packages(dist):
# type: (Distribution) -> bool
"""
Return True if given Distribution is installed in
sysconfig.get_python_lib().
"""
return dist_location(dist).startswith(normalize_path(site_packages))
def dist_is_editable(dist):
# type: (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=True, # type: bool
skip=stdlib_pkgs, # type: Container[str]
include_editables=True, # type: bool
editables_only=False, # type: bool
user_only=False, # type: bool
paths=None, # type: Optional[List[str]]
):
# type: (...) -> List[Distribution]
"""Return a list of installed Distribution objects.
Left for compatibility until direct pkg_resources uses are refactored out.
"""
from pipenv.patched.notpip._internal.metadata import get_default_environment, get_environment
from pipenv.patched.notpip._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):
# type: (str) -> Optional[Distribution]
"""Given a requirement name, return the installed Distribution object.
This searches from *all* distributions available in the environment, to
match the behavior of ``pkg_resources.get_distribution()``.
Left for compatibility until direct pkg_resources uses are refactored out.
"""
from pipenv.patched.notpip._internal.metadata import get_default_environment
from pipenv.patched.notpip._internal.metadata.pkg_resources import Distribution as _Dist
dist = get_default_environment().get_distribution(req_name)
if dist is None:
return None
return cast(_Dist, dist)._dist
def egg_link_path(dist):
# type: (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):
# type: (Distribution) -> str
"""
Get the site-packages location of this distribution. Generally
this is dist.location, except in the case of develop-installed
packages, where dist.location is the source code location, and we
want to know where the egg-link file is.
The returned location is normalized (in particular, with symlinks removed).
"""
egg_link = egg_link_path(dist)
if egg_link:
return normalize_path(egg_link)
return normalize_path(dist.location)
def write_output(msg, *args):
# type: (Any, Any) -> None
def write_output(msg: Any, *args: Any) -> None:
logger.info(msg, *args)
class StreamWrapper(StringIO):
orig_stream = None # type: TextIO
orig_stream: TextIO = None
@classmethod
def from_stream(cls, orig_stream):
# type: (TextIO) -> StreamWrapper
def from_stream(cls, orig_stream: TextIO) -> "StreamWrapper":
cls.orig_stream = orig_stream
return cls()
@@ -516,8 +346,7 @@ class StreamWrapper(StringIO):
@contextlib.contextmanager
def captured_output(stream_name):
# type: (str) -> Iterator[StreamWrapper]
def captured_output(stream_name: str) -> Iterator[StreamWrapper]:
"""Return a context manager used by captured_stdout/stdin/stderr
that temporarily replaces the sys stream *stream_name* with a StringIO.
@@ -531,8 +360,7 @@ def captured_output(stream_name):
setattr(sys, stream_name, orig_stdout)
def captured_stdout():
# type: () -> ContextManager[StreamWrapper]
def captured_stdout() -> ContextManager[StreamWrapper]:
"""Capture the output of sys.stdout:
with captured_stdout() as stdout:
@@ -544,8 +372,7 @@ def captured_stdout():
return captured_output("stdout")
def captured_stderr():
# type: () -> ContextManager[StreamWrapper]
def captured_stderr() -> ContextManager[StreamWrapper]:
"""
See captured_stdout().
"""
@@ -553,16 +380,14 @@ def captured_stderr():
# Simulates an enum
def enum(*sequential, **named):
# type: (*Any, **Any) -> Type[Any]
def enum(*sequential: Any, **named: Any) -> Type[Any]:
enums = dict(zip(sequential, range(len(sequential))), **named)
reverse = {value: key for key, value in enums.items()}
enums["reverse_mapping"] = reverse
return type("Enum", (), enums)
def build_netloc(host, port):
# type: (str, Optional[int]) -> str
def build_netloc(host: str, port: Optional[int]) -> str:
"""
Build a netloc from a host-port pair
"""
@@ -574,8 +399,7 @@ def build_netloc(host, port):
return f"{host}:{port}"
def build_url_from_netloc(netloc, scheme="https"):
# type: (str, str) -> str
def build_url_from_netloc(netloc: str, scheme: str = "https") -> str:
"""
Build a full URL from a netloc.
"""
@@ -585,8 +409,7 @@ def build_url_from_netloc(netloc, scheme="https"):
return f"{scheme}://{netloc}"
def parse_netloc(netloc):
# type: (str) -> Tuple[str, Optional[int]]
def parse_netloc(netloc: str) -> Tuple[str, Optional[int]]:
"""
Return the host-port pair from a netloc.
"""
@@ -595,8 +418,7 @@ def parse_netloc(netloc):
return parsed.hostname, parsed.port
def split_auth_from_netloc(netloc):
# type: (str) -> NetlocTuple
def split_auth_from_netloc(netloc: str) -> NetlocTuple:
"""
Parse out and remove the auth information from a netloc.
@@ -609,7 +431,7 @@ def split_auth_from_netloc(netloc):
# behaves if more than one @ is present (which can be checked using
# the password attribute of urlsplit()'s return value).
auth, netloc = netloc.rsplit("@", 1)
pw = None # type: Optional[str]
pw: Optional[str] = None
if ":" in auth:
# Split from the left because that's how urllib.parse.urlsplit()
# behaves if more than one : is present (which again can be checked
@@ -625,8 +447,7 @@ def split_auth_from_netloc(netloc):
return netloc, (user, pw)
def redact_netloc(netloc):
# type: (str) -> str
def redact_netloc(netloc: str) -> str:
"""
Replace the sensitive data in a netloc with "****", if it exists.
@@ -648,8 +469,9 @@ def redact_netloc(netloc):
)
def _transform_url(url, transform_netloc):
# type: (str, Callable[[str], Tuple[Any, ...]]) -> Tuple[str, NetlocTuple]
def _transform_url(
url: str, transform_netloc: Callable[[str], Tuple[Any, ...]]
) -> Tuple[str, NetlocTuple]:
"""Transform and replace netloc in a url.
transform_netloc is a function taking the netloc and returning a
@@ -667,18 +489,15 @@ def _transform_url(url, transform_netloc):
return surl, cast("NetlocTuple", netloc_tuple)
def _get_netloc(netloc):
# type: (str) -> NetlocTuple
def _get_netloc(netloc: str) -> NetlocTuple:
return split_auth_from_netloc(netloc)
def _redact_netloc(netloc):
# type: (str) -> Tuple[str,]
def _redact_netloc(netloc: str) -> Tuple[str]:
return (redact_netloc(netloc),)
def split_auth_netloc_from_url(url):
# type: (str) -> Tuple[str, str, Tuple[str, str]]
def split_auth_netloc_from_url(url: str) -> Tuple[str, str, Tuple[str, str]]:
"""
Parse a url into separate netloc, auth, and url with no auth.
@@ -688,41 +507,31 @@ def split_auth_netloc_from_url(url):
return url_without_auth, netloc, auth
def remove_auth_from_url(url):
# type: (str) -> str
def remove_auth_from_url(url: str) -> str:
"""Return a copy of url with 'username:password@' removed."""
# username/pass params are passed to subversion through flags
# and are not recognized in the url.
return _transform_url(url, _get_netloc)[0]
def redact_auth_from_url(url):
# type: (str) -> str
def redact_auth_from_url(url: str) -> str:
"""Replace the password in a given url with ****."""
return _transform_url(url, _redact_netloc)[0]
class HiddenText:
def __init__(
self,
secret, # type: str
redacted, # type: str
):
# type: (...) -> None
def __init__(self, secret: str, redacted: str) -> None:
self.secret = secret
self.redacted = redacted
def __repr__(self):
# type: (...) -> str
def __repr__(self) -> str:
return "<HiddenText {!r}>".format(str(self))
def __str__(self):
# type: (...) -> str
def __str__(self) -> str:
return self.redacted
# This is useful for testing.
def __eq__(self, other):
# type: (Any) -> bool
def __eq__(self, other: Any) -> bool:
if type(self) != type(other):
return False
@@ -731,19 +540,16 @@ class HiddenText:
return self.secret == other.secret
def hide_value(value):
# type: (str) -> HiddenText
def hide_value(value: str) -> HiddenText:
return HiddenText(value, redacted="****")
def hide_url(url):
# type: (str) -> HiddenText
def hide_url(url: str) -> HiddenText:
redacted = redact_auth_from_url(url)
return HiddenText(url, redacted=redacted)
def protect_pip_from_modification_on_windows(modifying_pip):
# type: (bool) -> None
def protect_pip_from_modification_on_windows(modifying_pip: bool) -> None:
"""Protection of pip.exe from modification on Windows
On Windows, any operation modifying pip should be run as:
@@ -769,14 +575,12 @@ def protect_pip_from_modification_on_windows(modifying_pip):
)
def is_console_interactive():
# type: () -> bool
def is_console_interactive() -> bool:
"""Is this console interactive?"""
return sys.stdin is not None and sys.stdin.isatty()
def hash_file(path, blocksize=1 << 20):
# type: (str, int) -> Tuple[Any, int]
def hash_file(path: str, blocksize: int = 1 << 20) -> Tuple[Any, int]:
"""Return (hash, length) for path using hashlib.sha256()"""
h = hashlib.sha256()
@@ -788,8 +592,7 @@ def hash_file(path, blocksize=1 << 20):
return h, length
def is_wheel_installed():
# type: () -> bool
def is_wheel_installed() -> bool:
"""
Return whether the wheel package is installed.
"""
@@ -801,8 +604,7 @@ def is_wheel_installed():
return True
def pairwise(iterable):
# type: (Iterable[Any]) -> Iterator[Tuple[Any, Any]]
def pairwise(iterable: Iterable[Any]) -> Iterator[Tuple[Any, Any]]:
"""
Return paired elements.
@@ -814,10 +616,9 @@ def pairwise(iterable):
def partition(
pred, # type: Callable[[T], bool]
iterable, # type: Iterable[T]
):
# type: (...) -> Tuple[Iterable[T], Iterable[T]]
pred: Callable[[T], bool],
iterable: Iterable[T],
) -> Tuple[Iterable[T], Iterable[T]]:
"""
Use a predicate to partition entries into false entries and true entries,
like
@@ -10,37 +10,29 @@ class KeyBasedCompareMixin:
__slots__ = ["_compare_key", "_defining_class"]
def __init__(self, key, defining_class):
# type: (Any, Type[KeyBasedCompareMixin]) -> None
def __init__(self, key: Any, defining_class: Type["KeyBasedCompareMixin"]) -> None:
self._compare_key = key
self._defining_class = defining_class
def __hash__(self):
# type: () -> int
def __hash__(self) -> int:
return hash(self._compare_key)
def __lt__(self, other):
# type: (Any) -> bool
def __lt__(self, other: Any) -> bool:
return self._compare(other, operator.__lt__)
def __le__(self, other):
# type: (Any) -> bool
def __le__(self, other: Any) -> bool:
return self._compare(other, operator.__le__)
def __gt__(self, other):
# type: (Any) -> bool
def __gt__(self, other: Any) -> bool:
return self._compare(other, operator.__gt__)
def __ge__(self, other):
# type: (Any) -> bool
def __ge__(self, other: Any) -> bool:
return self._compare(other, operator.__ge__)
def __eq__(self, other):
# type: (Any) -> bool
def __eq__(self, other: Any) -> bool:
return self._compare(other, operator.__eq__)
def _compare(self, other, method):
# type: (Any, Callable[[Any, Any], bool]) -> bool
def _compare(self, other: Any, method: Callable[[Any, Any], bool]) -> bool:
if not isinstance(other, self._defining_class):
return NotImplemented
@@ -1,20 +1,19 @@
import functools
import logging
from email.message import Message
from email.parser import FeedParser
from typing import Optional, Tuple
import re
from typing import NewType, Optional, Tuple, cast
from pipenv.patched.notpip._vendor import pkg_resources
from pipenv.patched.notpip._vendor.packaging import specifiers, version
from pipenv.patched.notpip._vendor.pkg_resources import Distribution
from pipenv.patched.notpip._vendor.packaging.requirements import Requirement
from pipenv.patched.notpip._internal.exceptions import NoneMetadataError
from pipenv.patched.notpip._internal.utils.misc import display_path
NormalizedExtra = NewType("NormalizedExtra", str)
logger = logging.getLogger(__name__)
def check_requires_python(requires_python, version_info):
# type: (Optional[str], Tuple[int, ...]) -> bool
def check_requires_python(
requires_python: Optional[str], version_info: Tuple[int, ...]
) -> bool:
"""
Check if the given Python version matches a "Requires-Python" specifier.
@@ -35,55 +34,24 @@ def check_requires_python(requires_python, version_info):
return python_version in requires_python_specifier
def get_metadata(dist):
# type: (Distribution) -> Message
@functools.lru_cache(maxsize=512)
def get_requirement(req_string: str) -> Requirement:
"""Construct a packaging.Requirement object with caching"""
# Parsing requirement strings is expensive, and is also expected to happen
# with a low diversity of different arguments (at least relative the number
# constructed). This method adds a cache to requirement object creation to
# minimize repeated parsing of the same string to construct equivalent
# Requirement objects.
return Requirement(req_string)
def safe_extra(extra: str) -> NormalizedExtra:
"""Convert an arbitrary string to a standard 'extra' name
Any runs of non-alphanumeric characters are replaced with a single '_',
and the result is always lowercased.
This function is duplicated from ``pkg_resources``. Note that this is not
the same to either ``canonicalize_name`` or ``_egg_link_name``.
"""
:raises NoneMetadataError: if the distribution reports `has_metadata()`
True but `get_metadata()` returns None.
"""
metadata_name = "METADATA"
if isinstance(dist, pkg_resources.DistInfoDistribution) and dist.has_metadata(
metadata_name
):
metadata = dist.get_metadata(metadata_name)
elif dist.has_metadata("PKG-INFO"):
metadata_name = "PKG-INFO"
metadata = dist.get_metadata(metadata_name)
else:
logger.warning("No metadata found in %s", display_path(dist.location))
metadata = ""
if metadata is None:
raise NoneMetadataError(dist, metadata_name)
feed_parser = FeedParser()
# The following line errors out if with a "NoneType" TypeError if
# passed metadata=None.
feed_parser.feed(metadata)
return feed_parser.close()
def get_requires_python(dist):
# type: (pkg_resources.Distribution) -> Optional[str]
"""
Return the "Requires-Python" metadata for a distribution, or None
if not present.
"""
pkg_info_dict = get_metadata(dist)
requires_python = pkg_info_dict.get("Requires-Python")
if requires_python is not None:
# Convert to a str to satisfy the type checker, since requires_python
# can be a Header object.
requires_python = str(requires_python)
return requires_python
def get_installer(dist):
# type: (Distribution) -> str
if dist.has_metadata("INSTALLER"):
for line in dist.get_metadata_lines("INSTALLER"):
if line.strip():
return line.strip()
return ""
return cast(NormalizedExtra, re.sub("[^A-Za-z0-9.-]+", "_", extra).lower())

Some files were not shown because too many files have changed in this diff Show More