mirror of
https://github.com/kennethreitz/pipenv.git
synced 2026-06-05 22:50:18 +00:00
Merge pull request #4215 from pypa/feature/update-pip-piptools
This commit is contained in:
@@ -0,0 +1 @@
|
||||
Updated vendored ``pip`` => ``20.0.2`` and ``pip-tools`` => ``5.0.0``.
|
||||
@@ -1 +1,18 @@
|
||||
__version__ = "19.3.1"
|
||||
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
|
||||
|
||||
if MYPY_CHECK_RUNNING:
|
||||
from typing import List, Optional
|
||||
|
||||
|
||||
__version__ = "20.0.2"
|
||||
|
||||
|
||||
def main(args=None):
|
||||
# type: (Optional[List[str]]) -> int
|
||||
"""This is an internal API only meant for use by pip's own console scripts.
|
||||
|
||||
For additional details, see https://github.com/pypa/pip/issues/7498.
|
||||
"""
|
||||
from pipenv.patched.notpip._internal.utils.entrypoints import _wrapper
|
||||
|
||||
return _wrapper(args)
|
||||
|
||||
@@ -13,7 +13,7 @@ if __package__ == '':
|
||||
path = os.path.dirname(os.path.dirname(__file__))
|
||||
sys.path.insert(0, path)
|
||||
|
||||
from pipenv.patched.notpip._internal.main import main as _main # isort:skip # noqa
|
||||
from pipenv.patched.notpip._internal.cli.main import main as _main # isort:skip # noqa
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(_main())
|
||||
|
||||
@@ -1,2 +1,18 @@
|
||||
#!/usr/bin/env python
|
||||
import pipenv.patched.notpip._internal.utils.inject_securetransport # noqa
|
||||
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
|
||||
|
||||
if MYPY_CHECK_RUNNING:
|
||||
from typing import Optional, List
|
||||
|
||||
|
||||
def main(args=None):
|
||||
# type: (Optional[List[str]]) -> int
|
||||
"""This is preserved for old console scripts that may still be referencing
|
||||
it.
|
||||
|
||||
For additional details, see https://github.com/pypa/pip/issues/7498.
|
||||
"""
|
||||
from pipenv.patched.notpip._internal.utils.entrypoints import _wrapper
|
||||
|
||||
return _wrapper(args)
|
||||
|
||||
@@ -23,7 +23,7 @@ from pipenv.patched.notpip._internal.utils.ui import open_spinner
|
||||
|
||||
if MYPY_CHECK_RUNNING:
|
||||
from typing import Tuple, Set, Iterable, Optional, List
|
||||
from pipenv.patched.notpip._internal.index import PackageFinder
|
||||
from pipenv.patched.notpip._internal.index.package_finder import PackageFinder
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -4,28 +4,38 @@
|
||||
# The following comment should be removed at some point in the future.
|
||||
# mypy: strict-optional=False
|
||||
|
||||
import errno
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
|
||||
from pipenv.patched.notpip._vendor.packaging.tags import interpreter_name, interpreter_version
|
||||
from pipenv.patched.notpip._vendor.packaging.utils import canonicalize_name
|
||||
|
||||
from pipenv.patched.notpip._internal.exceptions import InvalidWheelFilename
|
||||
from pipenv.patched.notpip._internal.models.link import Link
|
||||
from pipenv.patched.notpip._internal.utils.compat import expanduser
|
||||
from pipenv.patched.notpip._internal.models.wheel import Wheel
|
||||
from pipenv.patched.notpip._internal.utils.temp_dir import TempDirectory
|
||||
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
|
||||
from pipenv.patched.notpip._internal.utils.urls import path_to_url
|
||||
from pipenv.patched.notpip._internal.wheel import InvalidWheelFilename, Wheel
|
||||
|
||||
if MYPY_CHECK_RUNNING:
|
||||
from typing import Optional, Set, List, Any
|
||||
from pipenv.patched.notpip._internal.index import FormatControl
|
||||
from pipenv.patched.notpip._internal.pep425tags import Pep425Tag
|
||||
from typing import Optional, Set, List, Any, Dict
|
||||
|
||||
from pipenv.patched.notpip._vendor.packaging.tags import Tag
|
||||
|
||||
from pipenv.patched.notpip._internal.models.format_control import FormatControl
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _hash_dict(d):
|
||||
# type: (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()
|
||||
|
||||
|
||||
class Cache(object):
|
||||
"""An abstract class - provides cache directories for data from links
|
||||
|
||||
@@ -40,16 +50,19 @@ class Cache(object):
|
||||
def __init__(self, cache_dir, format_control, allowed_formats):
|
||||
# type: (str, FormatControl, Set[str]) -> None
|
||||
super(Cache, self).__init__()
|
||||
self.cache_dir = expanduser(cache_dir) if cache_dir else None
|
||||
assert not cache_dir or os.path.isabs(cache_dir)
|
||||
self.cache_dir = cache_dir or None
|
||||
self.format_control = format_control
|
||||
self.allowed_formats = allowed_formats
|
||||
|
||||
_valid_formats = {"source", "binary"}
|
||||
assert self.allowed_formats.union(_valid_formats) == _valid_formats
|
||||
|
||||
def _get_cache_path_parts(self, link):
|
||||
def _get_cache_path_parts_legacy(self, link):
|
||||
# type: (Link) -> List[str]
|
||||
"""Get parts of part that must be os.path.joined with cache_dir
|
||||
|
||||
Legacy cache key (pip < 20) for compatibility with older caches.
|
||||
"""
|
||||
|
||||
# We want to generate an url to use as our cache key, we don't want to
|
||||
@@ -73,30 +86,72 @@ class Cache(object):
|
||||
|
||||
return parts
|
||||
|
||||
def _get_candidates(self, link, package_name):
|
||||
def _get_cache_path_parts(self, link):
|
||||
# type: (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
|
||||
# and we don't care about those.
|
||||
key_parts = {"url": link.url_without_fragment}
|
||||
if link.hash_name is not None and link.hash is not None:
|
||||
key_parts[link.hash_name] = link.hash
|
||||
if link.subdirectory_fragment:
|
||||
key_parts["subdirectory"] = link.subdirectory_fragment
|
||||
|
||||
# Include interpreter name, major and minor version in cache key
|
||||
# to cope with ill-behaved sdists that build a different wheel
|
||||
# depending on the python version their setup.py is being run on,
|
||||
# and don't encode the difference in compatibility tags.
|
||||
# https://github.com/pypa/pip/issues/7296
|
||||
key_parts["interpreter_name"] = interpreter_name()
|
||||
key_parts["interpreter_version"] = interpreter_version()
|
||||
|
||||
# Encode our key url with sha224, we'll use this because it has similar
|
||||
# security properties to sha256, but with a shorter total output (and
|
||||
# thus less secure). However the differences don't make a lot of
|
||||
# difference for our use case here.
|
||||
hashed = _hash_dict(key_parts)
|
||||
|
||||
# We want to nest the directories some to prevent having a ton of top
|
||||
# level directories where we might run out of sub directories on some
|
||||
# FS.
|
||||
parts = [hashed[:2], hashed[2:4], hashed[4:6], hashed[6:]]
|
||||
|
||||
return parts
|
||||
|
||||
def _get_candidates(self, link, canonical_package_name):
|
||||
# type: (Link, Optional[str]) -> List[Any]
|
||||
can_not_cache = (
|
||||
not self.cache_dir or
|
||||
not package_name or
|
||||
not canonical_package_name or
|
||||
not link
|
||||
)
|
||||
if can_not_cache:
|
||||
return []
|
||||
|
||||
canonical_name = canonicalize_name(package_name)
|
||||
formats = self.format_control.get_allowed_formats(
|
||||
canonical_name
|
||||
canonical_package_name
|
||||
)
|
||||
if not self.allowed_formats.intersection(formats):
|
||||
return []
|
||||
|
||||
root = self.get_path_for_link(link)
|
||||
try:
|
||||
return os.listdir(root)
|
||||
except OSError as err:
|
||||
if err.errno in {errno.ENOENT, errno.ENOTDIR}:
|
||||
return []
|
||||
raise
|
||||
candidates = []
|
||||
path = self.get_path_for_link(link)
|
||||
if os.path.isdir(path):
|
||||
for candidate in os.listdir(path):
|
||||
candidates.append((candidate, path))
|
||||
# TODO remove legacy path lookup in pip>=21
|
||||
legacy_path = self.get_path_for_link_legacy(link)
|
||||
if os.path.isdir(legacy_path):
|
||||
for candidate in os.listdir(legacy_path):
|
||||
candidates.append((candidate, legacy_path))
|
||||
return candidates
|
||||
|
||||
def get_path_for_link_legacy(self, link):
|
||||
# type: (Link) -> str
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_path_for_link(self, link):
|
||||
# type: (Link) -> str
|
||||
@@ -108,7 +163,7 @@ class Cache(object):
|
||||
self,
|
||||
link, # type: Link
|
||||
package_name, # type: Optional[str]
|
||||
supported_tags, # type: List[Pep425Tag]
|
||||
supported_tags, # type: List[Tag]
|
||||
):
|
||||
# type: (...) -> Link
|
||||
"""Returns a link to a cached item if it exists, otherwise returns the
|
||||
@@ -116,13 +171,6 @@ class Cache(object):
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def _link_for_candidate(self, link, candidate):
|
||||
# type: (Link, str) -> Link
|
||||
root = self.get_path_for_link(link)
|
||||
path = os.path.join(root, candidate)
|
||||
|
||||
return Link(path_to_url(path))
|
||||
|
||||
def cleanup(self):
|
||||
# type: () -> None
|
||||
pass
|
||||
@@ -138,6 +186,11 @@ class SimpleWheelCache(Cache):
|
||||
cache_dir, format_control, {"binary"}
|
||||
)
|
||||
|
||||
def get_path_for_link_legacy(self, link):
|
||||
# type: (Link) -> str
|
||||
parts = self._get_cache_path_parts_legacy(link)
|
||||
return os.path.join(self.cache_dir, "wheels", *parts)
|
||||
|
||||
def get_path_for_link(self, link):
|
||||
# type: (Link) -> str
|
||||
"""Return a directory to store cached wheels for link
|
||||
@@ -163,27 +216,46 @@ class SimpleWheelCache(Cache):
|
||||
self,
|
||||
link, # type: Link
|
||||
package_name, # type: Optional[str]
|
||||
supported_tags, # type: List[Pep425Tag]
|
||||
supported_tags, # type: List[Tag]
|
||||
):
|
||||
# type: (...) -> Link
|
||||
candidates = []
|
||||
|
||||
for wheel_name in self._get_candidates(link, package_name):
|
||||
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
|
||||
):
|
||||
try:
|
||||
wheel = Wheel(wheel_name)
|
||||
except InvalidWheelFilename:
|
||||
continue
|
||||
if canonicalize_name(wheel.name) != canonical_package_name:
|
||||
logger.debug(
|
||||
"Ignoring cached wheel {} for {} as it "
|
||||
"does not match the expected distribution name {}.".format(
|
||||
wheel_name, link, package_name
|
||||
)
|
||||
)
|
||||
continue
|
||||
if not wheel.supported(supported_tags):
|
||||
# Built for a different python/arch/etc
|
||||
continue
|
||||
candidates.append(
|
||||
(wheel.support_index_min(supported_tags), wheel_name)
|
||||
(
|
||||
wheel.support_index_min(supported_tags),
|
||||
wheel_name,
|
||||
wheel_dir,
|
||||
)
|
||||
)
|
||||
|
||||
if not candidates:
|
||||
return link
|
||||
|
||||
return self._link_for_candidate(link, min(candidates)[1])
|
||||
_, wheel_name, wheel_dir = min(candidates)
|
||||
return Link(path_to_url(os.path.join(wheel_dir, wheel_name)))
|
||||
|
||||
|
||||
class EphemWheelCache(SimpleWheelCache):
|
||||
@@ -218,6 +290,10 @@ class WheelCache(Cache):
|
||||
self._wheel_cache = SimpleWheelCache(cache_dir, format_control)
|
||||
self._ephem_cache = EphemWheelCache(format_control)
|
||||
|
||||
def get_path_for_link_legacy(self, link):
|
||||
# type: (Link) -> str
|
||||
return self._wheel_cache.get_path_for_link_legacy(link)
|
||||
|
||||
def get_path_for_link(self, link):
|
||||
# type: (Link) -> str
|
||||
return self._wheel_cache.get_path_for_link(link)
|
||||
@@ -230,7 +306,7 @@ class WheelCache(Cache):
|
||||
self,
|
||||
link, # type: Link
|
||||
package_name, # type: Optional[str]
|
||||
supported_tags, # type: List[Pep425Tag]
|
||||
supported_tags, # type: List[Tag]
|
||||
):
|
||||
# type: (...) -> Link
|
||||
retval = self._wheel_cache.get(
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
"""Logic that powers autocompletion installed by ``pip completion``.
|
||||
"""
|
||||
|
||||
# The following comment should be removed at some point in the future.
|
||||
# mypy: disallow-untyped-defs=False
|
||||
|
||||
import optparse
|
||||
import os
|
||||
import sys
|
||||
from itertools import chain
|
||||
|
||||
from pipenv.patched.notpip._internal.cli.main_parser import create_main_parser
|
||||
from pipenv.patched.notpip._internal.commands import commands_dict, create_command
|
||||
from pipenv.patched.notpip._internal.utils.misc import get_installed_distributions
|
||||
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
|
||||
|
||||
if MYPY_CHECK_RUNNING:
|
||||
from typing import Any, Iterable, List, Optional
|
||||
|
||||
|
||||
def autocomplete():
|
||||
# type: () -> None
|
||||
"""Entry Point for completion of main and subcommand options.
|
||||
"""
|
||||
# Don't complete if user hasn't sourced bash_completion file.
|
||||
@@ -26,17 +29,18 @@ def autocomplete():
|
||||
except IndexError:
|
||||
current = ''
|
||||
|
||||
parser = create_main_parser()
|
||||
subcommands = list(commands_dict)
|
||||
options = []
|
||||
# subcommand
|
||||
try:
|
||||
subcommand_name = [w for w in cwords if w in subcommands][0]
|
||||
except IndexError:
|
||||
subcommand_name = None
|
||||
|
||||
parser = create_main_parser()
|
||||
# subcommand
|
||||
subcommand_name = None # type: Optional[str]
|
||||
for word in cwords:
|
||||
if word in subcommands:
|
||||
subcommand_name = word
|
||||
break
|
||||
# subcommand options
|
||||
if subcommand_name:
|
||||
if subcommand_name is not None:
|
||||
# special case: 'help' subcommand has no options
|
||||
if subcommand_name == 'help':
|
||||
sys.exit(1)
|
||||
@@ -76,8 +80,8 @@ def autocomplete():
|
||||
# get completion files and directories if ``completion_type`` is
|
||||
# ``<file>``, ``<dir>`` or ``<path>``
|
||||
if completion_type:
|
||||
options = auto_complete_paths(current, completion_type)
|
||||
options = ((opt, 0) for opt in options)
|
||||
paths = auto_complete_paths(current, completion_type)
|
||||
options = [(path, 0) for path in paths]
|
||||
for option in options:
|
||||
opt_label = option[0]
|
||||
# append '=' to options which require args
|
||||
@@ -89,22 +93,25 @@ def autocomplete():
|
||||
|
||||
opts = [i.option_list for i in parser.option_groups]
|
||||
opts.append(parser.option_list)
|
||||
opts = (o for it in opts for o in it)
|
||||
flattened_opts = chain.from_iterable(opts)
|
||||
if current.startswith('-'):
|
||||
for opt in opts:
|
||||
for opt in flattened_opts:
|
||||
if opt.help != optparse.SUPPRESS_HELP:
|
||||
subcommands += opt._long_opts + opt._short_opts
|
||||
else:
|
||||
# get completion type given cwords and all available options
|
||||
completion_type = get_path_completion_type(cwords, cword, opts)
|
||||
completion_type = get_path_completion_type(cwords, cword,
|
||||
flattened_opts)
|
||||
if completion_type:
|
||||
subcommands = auto_complete_paths(current, completion_type)
|
||||
subcommands = list(auto_complete_paths(current,
|
||||
completion_type))
|
||||
|
||||
print(' '.join([x for x in subcommands if x.startswith(current)]))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def get_path_completion_type(cwords, cword, opts):
|
||||
# type: (List[str], int, Iterable[Any]) -> Optional[str]
|
||||
"""Get the type of path completion (``file``, ``dir``, ``path`` or None)
|
||||
|
||||
:param cwords: same as the environmental variable ``COMP_WORDS``
|
||||
@@ -113,7 +120,7 @@ def get_path_completion_type(cwords, cword, opts):
|
||||
:return: path completion type (``file``, ``dir``, ``path`` or None)
|
||||
"""
|
||||
if cword < 2 or not cwords[cword - 2].startswith('-'):
|
||||
return
|
||||
return None
|
||||
for opt in opts:
|
||||
if opt.help == optparse.SUPPRESS_HELP:
|
||||
continue
|
||||
@@ -123,9 +130,11 @@ def get_path_completion_type(cwords, cword, opts):
|
||||
x in ('path', 'file', 'dir')
|
||||
for x in opt.metavar.split('/')):
|
||||
return opt.metavar
|
||||
return None
|
||||
|
||||
|
||||
def auto_complete_paths(current, completion_type):
|
||||
# type: (str, str) -> Iterable[str]
|
||||
"""If ``completion_type`` is ``file`` or ``path``, list all regular files
|
||||
and directories starting with ``current``; otherwise only list directories
|
||||
starting with ``current``.
|
||||
|
||||
@@ -31,8 +31,10 @@ from pipenv.patched.notpip._internal.exceptions import (
|
||||
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
|
||||
from pipenv.patched.notpip._internal.utils.misc import get_prog, normalize_path
|
||||
from pipenv.patched.notpip._internal.utils.temp_dir import global_tempdir_manager
|
||||
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
|
||||
from pipenv.patched.notpip._internal.utils.virtualenv import running_under_virtualenv
|
||||
|
||||
@@ -92,7 +94,7 @@ class Command(CommandContextMixIn):
|
||||
raise NotImplementedError
|
||||
|
||||
def parse_args(self, args):
|
||||
# type: (List[str]) -> Tuple
|
||||
# type: (List[str]) -> Tuple[Any, Any]
|
||||
# factored out for testability
|
||||
return self.parser.parse_args(args)
|
||||
|
||||
@@ -106,6 +108,10 @@ class Command(CommandContextMixIn):
|
||||
|
||||
def _main(self, args):
|
||||
# type: (List[str]) -> int
|
||||
# Intentionally set as early as possible so globally-managed temporary
|
||||
# directories are available to the rest of the code.
|
||||
self.enter_context(global_tempdir_manager())
|
||||
|
||||
options, args = self.parse_args(args)
|
||||
|
||||
# Set verbosity so that it can be used elsewhere.
|
||||
@@ -117,7 +123,10 @@ class Command(CommandContextMixIn):
|
||||
user_log_file=options.log,
|
||||
)
|
||||
|
||||
if sys.version_info[:2] == (2, 7):
|
||||
if (
|
||||
sys.version_info[:2] == (2, 7) and
|
||||
not options.no_python_version_warning
|
||||
):
|
||||
message = (
|
||||
"A future version of pip will drop support for Python 2.7. "
|
||||
"More details about Python 2 support in pip, can be found at "
|
||||
@@ -125,12 +134,23 @@ class Command(CommandContextMixIn):
|
||||
)
|
||||
if platform.python_implementation() == "CPython":
|
||||
message = (
|
||||
"Python 2.7 will reach the end of its life on January "
|
||||
"Python 2.7 reached the end of its life on January "
|
||||
"1st, 2020. Please upgrade your Python as Python 2.7 "
|
||||
"won't be maintained after that date. "
|
||||
"is no longer maintained. "
|
||||
) + message
|
||||
deprecated(message, replacement=None, gone_in=None)
|
||||
|
||||
if options.skip_requirements_regex:
|
||||
deprecated(
|
||||
"--skip-requirements-regex is unsupported and will be removed",
|
||||
replacement=(
|
||||
"manage requirements/constraints files explicitly, "
|
||||
"possibly generating them from metadata"
|
||||
),
|
||||
gone_in="20.1",
|
||||
issue=7297,
|
||||
)
|
||||
|
||||
# TODO: Try to get these passing down from the command?
|
||||
# without resorting to os.environ to hold these.
|
||||
# This also affects isolated builds and it should.
|
||||
@@ -149,6 +169,19 @@ class Command(CommandContextMixIn):
|
||||
)
|
||||
sys.exit(VIRTUALENV_NOT_FOUND)
|
||||
|
||||
if options.cache_dir:
|
||||
options.cache_dir = normalize_path(options.cache_dir)
|
||||
if not check_path_owner(options.cache_dir):
|
||||
logger.warning(
|
||||
"The directory '%s' or its parent directory is not owned "
|
||||
"or is not writable by the current user. The cache "
|
||||
"has been disabled. Check the permissions and owner of "
|
||||
"that directory. If executing pip with sudo, you may want "
|
||||
"sudo's -H flag.",
|
||||
options.cache_dir,
|
||||
)
|
||||
options.cache_dir = None
|
||||
|
||||
try:
|
||||
status = self.run(options, args)
|
||||
# FIXME: all commands should return an exit status
|
||||
|
||||
@@ -9,11 +9,11 @@ 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
|
||||
# mypy: disallow-untyped-defs=False
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import logging
|
||||
import os
|
||||
import textwrap
|
||||
import warnings
|
||||
from distutils.util import strtobool
|
||||
@@ -39,6 +39,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def raise_option_error(parser, option, msg):
|
||||
# type: (OptionParser, Option, str) -> None
|
||||
"""
|
||||
Raise an option parsing error using parser.error().
|
||||
|
||||
@@ -77,14 +78,15 @@ def check_install_build_global(options, check_options=None):
|
||||
check_options = options
|
||||
|
||||
def getname(n):
|
||||
# type: (str) -> Optional[Any]
|
||||
return getattr(check_options, n, None)
|
||||
names = ["build_options", "global_options", "install_options"]
|
||||
if any(map(getname, names)):
|
||||
control = options.format_control
|
||||
control.disallow_binaries()
|
||||
warnings.warn(
|
||||
'Disabling all use of wheels due to the use of --build-options '
|
||||
'/ --global-options / --install-options.', stacklevel=2,
|
||||
'Disabling all use of wheels due to the use of --build-option '
|
||||
'/ --global-option / --install-option.', stacklevel=2,
|
||||
)
|
||||
|
||||
|
||||
@@ -128,6 +130,17 @@ def check_dist_restriction(options, check_target=False):
|
||||
)
|
||||
|
||||
|
||||
def _path_option_check(option, opt, value):
|
||||
# type: (Option, str, str) -> str
|
||||
return os.path.expanduser(value)
|
||||
|
||||
|
||||
class PipOption(Option):
|
||||
TYPES = Option.TYPES + ("path",)
|
||||
TYPE_CHECKER = Option.TYPE_CHECKER.copy()
|
||||
TYPE_CHECKER["path"] = _path_option_check
|
||||
|
||||
|
||||
###########
|
||||
# options #
|
||||
###########
|
||||
@@ -215,10 +228,11 @@ progress_bar = partial(
|
||||
) # type: Callable[..., Option]
|
||||
|
||||
log = partial(
|
||||
Option,
|
||||
PipOption,
|
||||
"--log", "--log-file", "--local-log",
|
||||
dest="log",
|
||||
metavar="path",
|
||||
type="path",
|
||||
help="Path to a verbose appending log."
|
||||
) # type: Callable[..., Option]
|
||||
|
||||
@@ -289,19 +303,19 @@ def exists_action():
|
||||
|
||||
|
||||
cert = partial(
|
||||
Option,
|
||||
PipOption,
|
||||
'--cert',
|
||||
dest='cert',
|
||||
type='str',
|
||||
type='path',
|
||||
metavar='path',
|
||||
help="Path to alternate CA bundle.",
|
||||
) # type: Callable[..., Option]
|
||||
|
||||
client_cert = partial(
|
||||
Option,
|
||||
PipOption,
|
||||
'--client-cert',
|
||||
dest='client_cert',
|
||||
type='str',
|
||||
type='path',
|
||||
default=None,
|
||||
metavar='path',
|
||||
help="Path to SSL client certificate, a single file containing the "
|
||||
@@ -322,6 +336,7 @@ index_url = partial(
|
||||
|
||||
|
||||
def extra_index_url():
|
||||
# type: () -> Option
|
||||
return Option(
|
||||
'--extra-index-url',
|
||||
dest='extra_index_urls',
|
||||
@@ -410,12 +425,21 @@ def editable():
|
||||
)
|
||||
|
||||
|
||||
def _handle_src(option, opt_str, value, parser):
|
||||
# type: (Option, str, str, OptionParser) -> None
|
||||
value = os.path.abspath(value)
|
||||
setattr(parser.values, option.dest, value)
|
||||
|
||||
|
||||
src = partial(
|
||||
Option,
|
||||
PipOption,
|
||||
'--src', '--source', '--source-dir', '--source-directory',
|
||||
dest='src_dir',
|
||||
type='path',
|
||||
metavar='dir',
|
||||
default=get_src_prefix(),
|
||||
action='callback',
|
||||
callback=_handle_src,
|
||||
help='Directory to check out editable projects into. '
|
||||
'The default in a virtualenv is "<venv path>/src". '
|
||||
'The default for global installs is "<current dir>/src".'
|
||||
@@ -614,11 +638,12 @@ def prefer_binary():
|
||||
|
||||
|
||||
cache_dir = partial(
|
||||
Option,
|
||||
PipOption,
|
||||
"--cache-dir",
|
||||
dest="cache_dir",
|
||||
default=USER_CACHE_DIR,
|
||||
metavar="dir",
|
||||
type='path',
|
||||
help="Store the cache data in <dir>."
|
||||
) # type: Callable[..., Option]
|
||||
|
||||
@@ -669,11 +694,22 @@ no_deps = partial(
|
||||
help="Don't install package dependencies.",
|
||||
) # type: Callable[..., Option]
|
||||
|
||||
|
||||
def _handle_build_dir(option, opt, value, parser):
|
||||
# type: (Option, str, str, OptionParser) -> None
|
||||
if value:
|
||||
value = os.path.abspath(value)
|
||||
setattr(parser.values, option.dest, value)
|
||||
|
||||
|
||||
build_dir = partial(
|
||||
Option,
|
||||
PipOption,
|
||||
'-b', '--build', '--build-dir', '--build-directory',
|
||||
dest='build_dir',
|
||||
type='path',
|
||||
metavar='dir',
|
||||
action='callback',
|
||||
callback=_handle_build_dir,
|
||||
help='Directory to unpack packages into and build in. Note that '
|
||||
'an initial build still takes place in a temporary directory. '
|
||||
'The location of temporary directories can be controlled by setting '
|
||||
@@ -851,9 +887,10 @@ require_hashes = partial(
|
||||
|
||||
|
||||
list_path = partial(
|
||||
Option,
|
||||
PipOption,
|
||||
'--path',
|
||||
dest='path',
|
||||
type='path',
|
||||
action='append',
|
||||
help='Restrict to the specified installation path for listing '
|
||||
'packages (can be used multiple times).'
|
||||
@@ -868,6 +905,16 @@ def check_list_path_option(options):
|
||||
)
|
||||
|
||||
|
||||
no_python_version_warning = partial(
|
||||
Option,
|
||||
'--no-python-version-warning',
|
||||
dest='no_python_version_warning',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='Silence deprecation warnings for upcoming unsupported Pythons.',
|
||||
) # type: Callable[..., Option]
|
||||
|
||||
|
||||
##########
|
||||
# groups #
|
||||
##########
|
||||
@@ -895,6 +942,7 @@ general_group = {
|
||||
no_cache,
|
||||
disable_pip_version_check,
|
||||
no_color,
|
||||
no_python_version_warning,
|
||||
]
|
||||
} # type: Dict[str, Any]
|
||||
|
||||
|
||||
@@ -1,19 +1,25 @@
|
||||
# The following comment should be removed at some point in the future.
|
||||
# mypy: disallow-untyped-defs=False
|
||||
|
||||
from contextlib import contextmanager
|
||||
|
||||
from pipenv.patched.notpip._vendor.contextlib2 import ExitStack
|
||||
|
||||
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
|
||||
|
||||
if MYPY_CHECK_RUNNING:
|
||||
from typing import Iterator, ContextManager, TypeVar
|
||||
|
||||
_T = TypeVar('_T', covariant=True)
|
||||
|
||||
|
||||
class CommandContextMixIn(object):
|
||||
def __init__(self):
|
||||
# type: () -> None
|
||||
super(CommandContextMixIn, self).__init__()
|
||||
self._in_main_context = False
|
||||
self._main_context = ExitStack()
|
||||
|
||||
@contextmanager
|
||||
def main_context(self):
|
||||
# type: () -> Iterator[None]
|
||||
assert not self._in_main_context
|
||||
|
||||
self._in_main_context = True
|
||||
@@ -24,6 +30,7 @@ class CommandContextMixIn(object):
|
||||
self._in_main_context = False
|
||||
|
||||
def enter_context(self, context_provider):
|
||||
# type: (ContextManager[_T]) -> _T
|
||||
assert self._in_main_context
|
||||
|
||||
return self._main_context.enter_context(context_provider)
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
"""Primary application entrypoint.
|
||||
"""
|
||||
from __future__ import absolute_import
|
||||
|
||||
import locale
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
from pipenv.patched.notpip._internal.cli.autocompletion import autocomplete
|
||||
from pipenv.patched.notpip._internal.cli.main_parser import parse_command
|
||||
from pipenv.patched.notpip._internal.commands import create_command
|
||||
from pipenv.patched.notpip._internal.exceptions import PipError
|
||||
from pipenv.patched.notpip._internal.utils import deprecation
|
||||
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
|
||||
|
||||
if MYPY_CHECK_RUNNING:
|
||||
from typing import List, Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Do not import and use main() directly! Using it directly is actively
|
||||
# discouraged by pip's maintainers. The name, location and behavior of
|
||||
# this function is subject to change, so calling it directly is not
|
||||
# portable across different pip versions.
|
||||
|
||||
# In addition, running pip in-process is unsupported and unsafe. This is
|
||||
# elaborated in detail at
|
||||
# https://pip.pypa.io/en/stable/user_guide/#using-pip-from-your-program.
|
||||
# That document also provides suggestions that should work for nearly
|
||||
# all users that are considering importing and using main() directly.
|
||||
|
||||
# However, we know that certain users will still want to invoke pip
|
||||
# in-process. If you understand and accept the implications of using pip
|
||||
# in an unsupported manner, the best approach is to use runpy to avoid
|
||||
# depending on the exact location of this entry point.
|
||||
|
||||
# The following example shows how to use runpy to invoke pip in that
|
||||
# case:
|
||||
#
|
||||
# sys.argv = ["pip", your, args, here]
|
||||
# runpy.run_module("pip", run_name="__main__")
|
||||
#
|
||||
# Note that this will exit the process after running, unlike a direct
|
||||
# call to main. As it is not safe to do any processing after calling
|
||||
# main, this should not be an issue in practice.
|
||||
|
||||
def main(args=None):
|
||||
# type: (Optional[List[str]]) -> int
|
||||
if args is None:
|
||||
args = sys.argv[1:]
|
||||
|
||||
# Configure our deprecation warnings to be sent through loggers
|
||||
deprecation.install_warning_logger()
|
||||
|
||||
autocomplete()
|
||||
|
||||
try:
|
||||
cmd_name, cmd_args = parse_command(args)
|
||||
except PipError as exc:
|
||||
sys.stderr.write("ERROR: %s" % exc)
|
||||
sys.stderr.write(os.linesep)
|
||||
sys.exit(1)
|
||||
|
||||
# Needed for locale.getpreferredencoding(False) to work
|
||||
# in pip._internal.utils.encoding.auto_decode
|
||||
try:
|
||||
locale.setlocale(locale.LC_ALL, '')
|
||||
except locale.Error as e:
|
||||
# setlocale can apparently crash if locale are uninitialized
|
||||
logger.debug("Ignoring error %s when setting locale", e)
|
||||
command = create_command(cmd_name, isolated=("--isolated" in cmd_args))
|
||||
|
||||
return command.main(cmd_args)
|
||||
@@ -5,18 +5,17 @@ needing download / PackageFinder capability don't unnecessarily import the
|
||||
PackageFinder machinery and all its vendored dependencies, etc.
|
||||
"""
|
||||
|
||||
# The following comment should be removed at some point in the future.
|
||||
# mypy: disallow-untyped-defs=False
|
||||
|
||||
import logging
|
||||
import os
|
||||
from functools import partial
|
||||
|
||||
from pipenv.patched.notpip._internal.cli.base_command import Command
|
||||
from pipenv.patched.notpip._internal.cli.command_context import CommandContextMixIn
|
||||
from pipenv.patched.notpip._internal.exceptions import CommandError
|
||||
from pipenv.patched.notpip._internal.index import PackageFinder
|
||||
from pipenv.patched.notpip._internal.index.package_finder import PackageFinder
|
||||
from pipenv.patched.notpip._internal.legacy_resolve import Resolver
|
||||
from pipenv.patched.notpip._internal.models.selection_prefs import SelectionPreferences
|
||||
from pipenv.patched.notpip._internal.network.download import Downloader
|
||||
from pipenv.patched.notpip._internal.network.session import PipSession
|
||||
from pipenv.patched.notpip._internal.operations.prepare import RequirementPreparer
|
||||
from pipenv.patched.notpip._internal.req.constructors import (
|
||||
@@ -29,7 +28,6 @@ from pipenv.patched.notpip._internal.self_outdated_check import (
|
||||
make_link_collector,
|
||||
pip_self_version_check,
|
||||
)
|
||||
from pipenv.patched.notpip._internal.utils.misc import normalize_path
|
||||
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
|
||||
|
||||
if MYPY_CHECK_RUNNING:
|
||||
@@ -41,6 +39,8 @@ if MYPY_CHECK_RUNNING:
|
||||
from pipenv.patched.notpip._internal.req.req_tracker import RequirementTracker
|
||||
from pipenv.patched.notpip._internal.utils.temp_dir import TempDirectory
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SessionCommandMixin(CommandContextMixIn):
|
||||
|
||||
@@ -48,11 +48,13 @@ class SessionCommandMixin(CommandContextMixIn):
|
||||
A class mixin for command classes needing _build_session().
|
||||
"""
|
||||
def __init__(self):
|
||||
# type: () -> None
|
||||
super(SessionCommandMixin, self).__init__()
|
||||
self._session = None # Optional[PipSession]
|
||||
|
||||
@classmethod
|
||||
def _get_index_urls(cls, options):
|
||||
# type: (Values) -> Optional[List[str]]
|
||||
"""Return a list of index urls from user-provided options."""
|
||||
index_urls = []
|
||||
if not getattr(options, "no_index", False):
|
||||
@@ -70,13 +72,18 @@ class SessionCommandMixin(CommandContextMixIn):
|
||||
"""Get a default-managed session."""
|
||||
if self._session is None:
|
||||
self._session = self.enter_context(self._build_session(options))
|
||||
# there's no type annotation on requests.Session, so it's
|
||||
# automatically ContextManager[Any] and self._session becomes Any,
|
||||
# then https://github.com/python/mypy/issues/7696 kicks in
|
||||
assert self._session is not None
|
||||
return self._session
|
||||
|
||||
def _build_session(self, options, retries=None, timeout=None):
|
||||
# type: (Values, Optional[int], Optional[int]) -> PipSession
|
||||
assert not options.cache_dir or os.path.isabs(options.cache_dir)
|
||||
session = PipSession(
|
||||
cache=(
|
||||
normalize_path(os.path.join(options.cache_dir, "http"))
|
||||
os.path.join(options.cache_dir, "http")
|
||||
if options.cache_dir else None
|
||||
),
|
||||
retries=retries if retries is not None else options.retries,
|
||||
@@ -149,6 +156,9 @@ class RequirementCommand(IndexGroupCommand):
|
||||
temp_build_dir, # type: TempDirectory
|
||||
options, # type: Values
|
||||
req_tracker, # type: RequirementTracker
|
||||
session, # type: PipSession
|
||||
finder, # type: PackageFinder
|
||||
use_user_site, # type: bool
|
||||
download_dir=None, # type: str
|
||||
wheel_download_dir=None, # type: str
|
||||
):
|
||||
@@ -156,22 +166,27 @@ class RequirementCommand(IndexGroupCommand):
|
||||
"""
|
||||
Create a RequirementPreparer instance for the given parameters.
|
||||
"""
|
||||
downloader = Downloader(session, progress_bar=options.progress_bar)
|
||||
|
||||
temp_build_dir_path = temp_build_dir.path
|
||||
assert temp_build_dir_path is not None
|
||||
|
||||
return RequirementPreparer(
|
||||
build_dir=temp_build_dir_path,
|
||||
src_dir=options.src_dir,
|
||||
download_dir=download_dir,
|
||||
wheel_download_dir=wheel_download_dir,
|
||||
progress_bar=options.progress_bar,
|
||||
build_isolation=options.build_isolation,
|
||||
req_tracker=req_tracker,
|
||||
downloader=downloader,
|
||||
finder=finder,
|
||||
require_hashes=options.require_hashes,
|
||||
use_user_site=use_user_site,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def make_resolver(
|
||||
preparer, # type: RequirementPreparer
|
||||
session, # type: PipSession
|
||||
finder, # type: PackageFinder
|
||||
options, # type: Values
|
||||
wheel_cache=None, # type: Optional[WheelCache]
|
||||
@@ -195,7 +210,6 @@ class RequirementCommand(IndexGroupCommand):
|
||||
)
|
||||
return Resolver(
|
||||
preparer=preparer,
|
||||
session=session,
|
||||
finder=finder,
|
||||
make_install_req=make_install_req,
|
||||
use_user_site=use_user_site,
|
||||
@@ -204,7 +218,7 @@ class RequirementCommand(IndexGroupCommand):
|
||||
ignore_requires_python=ignore_requires_python,
|
||||
force_reinstall=force_reinstall,
|
||||
upgrade_strategy=upgrade_strategy,
|
||||
py_version_info=py_version_info
|
||||
py_version_info=py_version_info,
|
||||
)
|
||||
|
||||
def populate_requirement_set(
|
||||
@@ -220,9 +234,6 @@ class RequirementCommand(IndexGroupCommand):
|
||||
"""
|
||||
Marshal cmd line args into a requirement set.
|
||||
"""
|
||||
# NOTE: As a side-effect, options.require_hashes and
|
||||
# requirement_set.require_hashes may be updated
|
||||
|
||||
for filename in options.constraints:
|
||||
for req_to_add in parse_requirements(
|
||||
filename,
|
||||
@@ -250,6 +261,7 @@ class RequirementCommand(IndexGroupCommand):
|
||||
req_to_add.is_direct = True
|
||||
requirement_set.add_requirement(req_to_add)
|
||||
|
||||
# NOTE: options.require_hashes may be set if --require-hashes is True
|
||||
for filename in options.requirements:
|
||||
for req_to_add in parse_requirements(
|
||||
filename,
|
||||
@@ -258,9 +270,14 @@ class RequirementCommand(IndexGroupCommand):
|
||||
use_pep517=options.use_pep517):
|
||||
req_to_add.is_direct = True
|
||||
requirement_set.add_requirement(req_to_add)
|
||||
# If --require-hashes was a line in a requirements file, tell
|
||||
# RequirementSet about it:
|
||||
requirement_set.require_hashes = options.require_hashes
|
||||
|
||||
# If any requirement has hash options, enable hash checking.
|
||||
requirements = (
|
||||
requirement_set.unnamed_requirements +
|
||||
list(requirement_set.requirements.values())
|
||||
)
|
||||
if any(req.has_hash_options for req in requirements):
|
||||
options.require_hashes = True
|
||||
|
||||
if not (args or options.editables or options.requirements):
|
||||
opts = {'name': self.name}
|
||||
@@ -274,6 +291,18 @@ class RequirementCommand(IndexGroupCommand):
|
||||
'You must give at least one requirement to %(name)s '
|
||||
'(see "pip help %(name)s")' % opts)
|
||||
|
||||
@staticmethod
|
||||
def trace_basic_info(finder):
|
||||
# type: (PackageFinder) -> None
|
||||
"""
|
||||
Trace basic information about the provided objects.
|
||||
"""
|
||||
# Display where finder is looking for packages
|
||||
search_scope = finder.search_scope
|
||||
locations = search_scope.get_formatted_locations()
|
||||
if locations:
|
||||
logger.info(locations)
|
||||
|
||||
def _build_package_finder(
|
||||
self,
|
||||
options, # type: Values
|
||||
|
||||
@@ -5,8 +5,11 @@ from __future__ import absolute_import
|
||||
|
||||
import locale
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
from pipenv.patched.notpip._vendor.certifi import where
|
||||
|
||||
from pipenv.patched.notpip._internal.cli import cmdoptions
|
||||
from pipenv.patched.notpip._internal.cli.base_command import Command
|
||||
from pipenv.patched.notpip._internal.cli.cmdoptions import make_target_python
|
||||
@@ -14,17 +17,16 @@ from pipenv.patched.notpip._internal.cli.status_codes import SUCCESS
|
||||
from pipenv.patched.notpip._internal.utils.logging import indent_log
|
||||
from pipenv.patched.notpip._internal.utils.misc import get_pip_version
|
||||
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
|
||||
from pipenv.patched.notpip._internal.wheel import format_tag
|
||||
|
||||
if MYPY_CHECK_RUNNING:
|
||||
from typing import Any, List
|
||||
from typing import Any, List, Optional
|
||||
from optparse import Values
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def show_value(name, value):
|
||||
# type: (str, str) -> None
|
||||
# type: (str, Optional[str]) -> None
|
||||
logger.info('{}: {}'.format(name, value))
|
||||
|
||||
|
||||
@@ -65,7 +67,7 @@ def show_tags(options):
|
||||
|
||||
with indent_log():
|
||||
for tag in tags:
|
||||
logger.info(format_tag(tag))
|
||||
logger.info(str(tag))
|
||||
|
||||
if tags_limited:
|
||||
msg = (
|
||||
@@ -75,6 +77,25 @@ def show_tags(options):
|
||||
logger.info(msg)
|
||||
|
||||
|
||||
def ca_bundle_info(config):
|
||||
levels = set()
|
||||
for key, value in config.items():
|
||||
levels.add(key.split('.')[0])
|
||||
|
||||
if not levels:
|
||||
return "Not specified"
|
||||
|
||||
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'
|
||||
|
||||
levels.remove('global')
|
||||
return ", ".join(levels)
|
||||
|
||||
|
||||
class DebugCommand(Command):
|
||||
"""
|
||||
Display debug information.
|
||||
@@ -90,6 +111,7 @@ class DebugCommand(Command):
|
||||
cmd_opts = self.cmd_opts
|
||||
cmdoptions.add_target_python_options(cmd_opts)
|
||||
self.parser.insert_option_group(0, cmd_opts)
|
||||
self.parser.config.load()
|
||||
|
||||
def run(self, options, args):
|
||||
# type: (Values, List[Any]) -> int
|
||||
@@ -110,6 +132,11 @@ class DebugCommand(Command):
|
||||
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("pip._vendor.certifi.where()", where())
|
||||
|
||||
show_tags(options)
|
||||
|
||||
return SUCCESS
|
||||
|
||||
@@ -10,8 +10,7 @@ from pipenv.patched.notpip._internal.cli import cmdoptions
|
||||
from pipenv.patched.notpip._internal.cli.cmdoptions import make_target_python
|
||||
from pipenv.patched.notpip._internal.cli.req_command import RequirementCommand
|
||||
from pipenv.patched.notpip._internal.req import RequirementSet
|
||||
from pipenv.patched.notpip._internal.req.req_tracker import RequirementTracker
|
||||
from pipenv.patched.notpip._internal.utils.filesystem import check_path_owner
|
||||
from pipenv.patched.notpip._internal.req.req_tracker import get_requirement_tracker
|
||||
from pipenv.patched.notpip._internal.utils.misc import ensure_dir, normalize_path, write_output
|
||||
from pipenv.patched.notpip._internal.utils.temp_dir import TempDirectory
|
||||
|
||||
@@ -86,7 +85,6 @@ class DownloadCommand(RequirementCommand):
|
||||
|
||||
cmdoptions.check_dist_restriction(options)
|
||||
|
||||
options.src_dir = os.path.abspath(options.src_dir)
|
||||
options.download_dir = normalize_path(options.download_dir)
|
||||
|
||||
ensure_dir(options.download_dir)
|
||||
@@ -100,24 +98,12 @@ class DownloadCommand(RequirementCommand):
|
||||
target_python=target_python,
|
||||
)
|
||||
build_delete = (not (options.no_clean or options.build_dir))
|
||||
if options.cache_dir and not check_path_owner(options.cache_dir):
|
||||
logger.warning(
|
||||
"The directory '%s' or its parent directory is not owned "
|
||||
"by the current user and caching wheels has been "
|
||||
"disabled. check the permissions and owner of that "
|
||||
"directory. If executing pip with sudo, you may want "
|
||||
"sudo's -H flag.",
|
||||
options.cache_dir,
|
||||
)
|
||||
options.cache_dir = None
|
||||
|
||||
with RequirementTracker() as req_tracker, TempDirectory(
|
||||
with get_requirement_tracker() as req_tracker, TempDirectory(
|
||||
options.build_dir, delete=build_delete, kind="download"
|
||||
) as directory:
|
||||
|
||||
requirement_set = RequirementSet(
|
||||
require_hashes=options.require_hashes,
|
||||
)
|
||||
requirement_set = RequirementSet()
|
||||
self.populate_requirement_set(
|
||||
requirement_set,
|
||||
args,
|
||||
@@ -131,16 +117,21 @@ class DownloadCommand(RequirementCommand):
|
||||
temp_build_dir=directory,
|
||||
options=options,
|
||||
req_tracker=req_tracker,
|
||||
session=session,
|
||||
finder=finder,
|
||||
download_dir=options.download_dir,
|
||||
use_user_site=False,
|
||||
)
|
||||
|
||||
resolver = self.make_resolver(
|
||||
preparer=preparer,
|
||||
finder=finder,
|
||||
session=session,
|
||||
options=options,
|
||||
py_version_info=options.python_version,
|
||||
)
|
||||
|
||||
self.trace_basic_info(finder)
|
||||
|
||||
resolver.resolve(requirement_set)
|
||||
|
||||
downloaded = ' '.join([
|
||||
|
||||
@@ -12,6 +12,7 @@ import logging
|
||||
import operator
|
||||
import os
|
||||
import shutil
|
||||
import site
|
||||
from optparse import SUPPRESS_HELP
|
||||
|
||||
from pipenv.patched.notpip._vendor import pkg_resources
|
||||
@@ -30,8 +31,10 @@ from pipenv.patched.notpip._internal.exceptions import (
|
||||
from pipenv.patched.notpip._internal.locations import distutils_scheme
|
||||
from pipenv.patched.notpip._internal.operations.check import check_install_conflicts
|
||||
from pipenv.patched.notpip._internal.req import RequirementSet, install_given_reqs
|
||||
from pipenv.patched.notpip._internal.req.req_tracker import RequirementTracker
|
||||
from pipenv.patched.notpip._internal.utils.filesystem import check_path_owner
|
||||
from pipenv.patched.notpip._internal.req.req_tracker import get_requirement_tracker
|
||||
from pipenv.patched.notpip._internal.utils.deprecation import deprecated
|
||||
from pipenv.patched.notpip._internal.utils.distutils_args import parse_distutils_args
|
||||
from pipenv.patched.notpip._internal.utils.filesystem import test_writable_dir
|
||||
from pipenv.patched.notpip._internal.utils.misc import (
|
||||
ensure_dir,
|
||||
get_installed_version,
|
||||
@@ -41,62 +44,20 @@ from pipenv.patched.notpip._internal.utils.misc import (
|
||||
from pipenv.patched.notpip._internal.utils.temp_dir import TempDirectory
|
||||
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
|
||||
from pipenv.patched.notpip._internal.utils.virtualenv import virtualenv_no_global
|
||||
from pipenv.patched.notpip._internal.wheel import WheelBuilder
|
||||
from pipenv.patched.notpip._internal.wheel_builder import build, should_build_for_install_command
|
||||
|
||||
if MYPY_CHECK_RUNNING:
|
||||
from optparse import Values
|
||||
from typing import Any, List, Optional
|
||||
from typing import Any, Iterable, List, Optional
|
||||
|
||||
from pipenv.patched.notpip._internal.models.format_control import FormatControl
|
||||
from pipenv.patched.notpip._internal.req.req_install import InstallRequirement
|
||||
from pipenv.patched.notpip._internal.wheel import BinaryAllowedPredicate
|
||||
from pipenv.patched.notpip._internal.wheel_builder import BinaryAllowedPredicate
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def is_wheel_installed():
|
||||
"""
|
||||
Return whether the wheel package is installed.
|
||||
"""
|
||||
try:
|
||||
import wheel # noqa: F401
|
||||
except ImportError:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def build_wheels(
|
||||
builder, # type: WheelBuilder
|
||||
pep517_requirements, # type: List[InstallRequirement]
|
||||
legacy_requirements, # type: List[InstallRequirement]
|
||||
):
|
||||
# type: (...) -> List[InstallRequirement]
|
||||
"""
|
||||
Build wheels for requirements, depending on whether wheel is installed.
|
||||
"""
|
||||
# We don't build wheels for legacy requirements if wheel is not installed.
|
||||
should_build_legacy = is_wheel_installed()
|
||||
|
||||
# Always build PEP 517 requirements
|
||||
build_failures = builder.build(
|
||||
pep517_requirements,
|
||||
should_unpack=True,
|
||||
)
|
||||
|
||||
if should_build_legacy:
|
||||
# We don't care about failures building legacy
|
||||
# requirements, as we'll fall through to a direct
|
||||
# install for those.
|
||||
builder.build(
|
||||
legacy_requirements,
|
||||
should_unpack=True,
|
||||
)
|
||||
|
||||
return build_failures
|
||||
|
||||
|
||||
def get_check_binary_allowed(format_control):
|
||||
# type: (FormatControl) -> BinaryAllowedPredicate
|
||||
def check_binary_allowed(req):
|
||||
@@ -285,26 +246,17 @@ class InstallCommand(RequirementCommand):
|
||||
if options.upgrade:
|
||||
upgrade_strategy = options.upgrade_strategy
|
||||
|
||||
if options.build_dir:
|
||||
options.build_dir = os.path.abspath(options.build_dir)
|
||||
|
||||
cmdoptions.check_dist_restriction(options, check_target=True)
|
||||
|
||||
options.src_dir = os.path.abspath(options.src_dir)
|
||||
install_options = options.install_options or []
|
||||
if options.use_user_site:
|
||||
if options.prefix_path:
|
||||
raise CommandError(
|
||||
"Can not combine '--user' and '--prefix' as they imply "
|
||||
"different installation locations"
|
||||
)
|
||||
if virtualenv_no_global():
|
||||
raise InstallationError(
|
||||
"Can not perform a '--user' install. User site-packages "
|
||||
"are not visible in this virtualenv."
|
||||
)
|
||||
install_options.append('--user')
|
||||
install_options.append('--prefix=')
|
||||
|
||||
options.use_user_site = decide_user_install(
|
||||
options.use_user_site,
|
||||
prefix_path=options.prefix_path,
|
||||
target_dir=options.target_dir,
|
||||
root_path=options.root_path,
|
||||
isolated_mode=options.isolated_mode,
|
||||
)
|
||||
|
||||
target_temp_dir = None # type: Optional[TempDirectory]
|
||||
target_temp_dir_path = None # type: Optional[str]
|
||||
@@ -321,7 +273,6 @@ class InstallCommand(RequirementCommand):
|
||||
# Create a target directory for using with the target option
|
||||
target_temp_dir = TempDirectory(kind="target")
|
||||
target_temp_dir_path = target_temp_dir.path
|
||||
install_options.append('--home=' + target_temp_dir_path)
|
||||
|
||||
global_options = options.global_options or []
|
||||
|
||||
@@ -337,22 +288,10 @@ class InstallCommand(RequirementCommand):
|
||||
build_delete = (not (options.no_clean or options.build_dir))
|
||||
wheel_cache = WheelCache(options.cache_dir, options.format_control)
|
||||
|
||||
if options.cache_dir and not check_path_owner(options.cache_dir):
|
||||
logger.warning(
|
||||
"The directory '%s' or its parent directory is not owned "
|
||||
"by the current user and caching wheels has been "
|
||||
"disabled. check the permissions and owner of that "
|
||||
"directory. If executing pip with sudo, you may want "
|
||||
"sudo's -H flag.",
|
||||
options.cache_dir,
|
||||
)
|
||||
options.cache_dir = None
|
||||
|
||||
with RequirementTracker() as req_tracker, TempDirectory(
|
||||
with get_requirement_tracker() as req_tracker, TempDirectory(
|
||||
options.build_dir, delete=build_delete, kind="install"
|
||||
) as directory:
|
||||
requirement_set = RequirementSet(
|
||||
require_hashes=options.require_hashes,
|
||||
check_supported_wheels=not options.target_dir,
|
||||
)
|
||||
|
||||
@@ -361,15 +300,22 @@ class InstallCommand(RequirementCommand):
|
||||
requirement_set, args, options, finder, session,
|
||||
wheel_cache
|
||||
)
|
||||
|
||||
warn_deprecated_install_options(
|
||||
requirement_set, options.install_options
|
||||
)
|
||||
|
||||
preparer = self.make_requirement_preparer(
|
||||
temp_build_dir=directory,
|
||||
options=options,
|
||||
req_tracker=req_tracker,
|
||||
session=session,
|
||||
finder=finder,
|
||||
use_user_site=options.use_user_site,
|
||||
)
|
||||
resolver = self.make_resolver(
|
||||
preparer=preparer,
|
||||
finder=finder,
|
||||
session=session,
|
||||
options=options,
|
||||
wheel_cache=wheel_cache,
|
||||
use_user_site=options.use_user_site,
|
||||
@@ -379,6 +325,9 @@ class InstallCommand(RequirementCommand):
|
||||
upgrade_strategy=upgrade_strategy,
|
||||
use_pep517=options.use_pep517,
|
||||
)
|
||||
|
||||
self.trace_basic_info(finder)
|
||||
|
||||
resolver.resolve(requirement_set)
|
||||
|
||||
try:
|
||||
@@ -396,34 +345,34 @@ class InstallCommand(RequirementCommand):
|
||||
check_binary_allowed = get_check_binary_allowed(
|
||||
finder.format_control
|
||||
)
|
||||
# Consider legacy and PEP517-using requirements separately
|
||||
legacy_requirements = []
|
||||
pep517_requirements = []
|
||||
for req in requirement_set.requirements.values():
|
||||
if req.use_pep517:
|
||||
pep517_requirements.append(req)
|
||||
else:
|
||||
legacy_requirements.append(req)
|
||||
|
||||
wheel_builder = WheelBuilder(
|
||||
preparer, wheel_cache,
|
||||
build_options=[], global_options=[],
|
||||
check_binary_allowed=check_binary_allowed,
|
||||
)
|
||||
reqs_to_build = [
|
||||
r for r in requirement_set.requirements.values()
|
||||
if should_build_for_install_command(
|
||||
r, check_binary_allowed
|
||||
)
|
||||
]
|
||||
|
||||
build_failures = build_wheels(
|
||||
builder=wheel_builder,
|
||||
pep517_requirements=pep517_requirements,
|
||||
legacy_requirements=legacy_requirements,
|
||||
_, build_failures = build(
|
||||
reqs_to_build,
|
||||
wheel_cache=wheel_cache,
|
||||
build_options=[],
|
||||
global_options=[],
|
||||
)
|
||||
|
||||
# If we're using PEP 517, we cannot do a direct install
|
||||
# so we fail here.
|
||||
if build_failures:
|
||||
# We don't care about failures building legacy
|
||||
# requirements, as we'll fall through to a direct
|
||||
# install for those.
|
||||
pep517_build_failures = [
|
||||
r for r in build_failures if r.use_pep517
|
||||
]
|
||||
if pep517_build_failures:
|
||||
raise InstallationError(
|
||||
"Could not build wheels for {} which use"
|
||||
" PEP 517 and cannot be installed directly".format(
|
||||
", ".join(r.name for r in build_failures)))
|
||||
", ".join(r.name for r in pep517_build_failures)))
|
||||
|
||||
to_install = resolver.get_installation_order(
|
||||
requirement_set
|
||||
@@ -464,13 +413,13 @@ class InstallCommand(RequirementCommand):
|
||||
)
|
||||
working_set = pkg_resources.WorkingSet(lib_locations)
|
||||
|
||||
reqs = sorted(installed, key=operator.attrgetter('name'))
|
||||
installed.sort(key=operator.attrgetter('name'))
|
||||
items = []
|
||||
for req in reqs:
|
||||
item = req.name
|
||||
for result in installed:
|
||||
item = result.name
|
||||
try:
|
||||
installed_version = get_installed_version(
|
||||
req.name, working_set=working_set
|
||||
result.name, working_set=working_set
|
||||
)
|
||||
if installed_version:
|
||||
item += '-' + installed_version
|
||||
@@ -595,6 +544,127 @@ def get_lib_location_guesses(*args, **kwargs):
|
||||
return [scheme['purelib'], scheme['platlib']]
|
||||
|
||||
|
||||
def site_packages_writable(**kwargs):
|
||||
return all(
|
||||
test_writable_dir(d) for d in set(get_lib_location_guesses(**kwargs))
|
||||
)
|
||||
|
||||
|
||||
def decide_user_install(
|
||||
use_user_site, # type: Optional[bool]
|
||||
prefix_path=None, # type: Optional[str]
|
||||
target_dir=None, # type: Optional[str]
|
||||
root_path=None, # type: Optional[str]
|
||||
isolated_mode=False, # type: bool
|
||||
):
|
||||
# type: (...) -> bool
|
||||
"""Determine whether to do a user install based on the input options.
|
||||
|
||||
If use_user_site is False, no additional checks are done.
|
||||
If use_user_site is True, it is checked for compatibility with other
|
||||
options.
|
||||
If use_user_site is None, the default behaviour depends on the environment,
|
||||
which is provided by the other arguments.
|
||||
"""
|
||||
# In some cases (config from tox), use_user_site can be set to an integer
|
||||
# rather than a bool, which 'use_user_site is False' wouldn't catch.
|
||||
if (use_user_site is not None) and (not use_user_site):
|
||||
logger.debug("Non-user install by explicit request")
|
||||
return False
|
||||
|
||||
if use_user_site:
|
||||
if prefix_path:
|
||||
raise CommandError(
|
||||
"Can not combine '--user' and '--prefix' as they imply "
|
||||
"different installation locations"
|
||||
)
|
||||
if virtualenv_no_global():
|
||||
raise InstallationError(
|
||||
"Can not perform a '--user' install. User site-packages "
|
||||
"are not visible in this virtualenv."
|
||||
)
|
||||
logger.debug("User install by explicit request")
|
||||
return True
|
||||
|
||||
# If we are here, user installs have not been explicitly requested/avoided
|
||||
assert use_user_site is None
|
||||
|
||||
# user install incompatible with --prefix/--target
|
||||
if prefix_path or target_dir:
|
||||
logger.debug("Non-user install due to --prefix or --target option")
|
||||
return False
|
||||
|
||||
# If user installs are not enabled, choose a non-user install
|
||||
if not site.ENABLE_USER_SITE:
|
||||
logger.debug("Non-user install because user site-packages disabled")
|
||||
return False
|
||||
|
||||
# If we have permission for a non-user install, do that,
|
||||
# otherwise do a user install.
|
||||
if site_packages_writable(root=root_path, isolated=isolated_mode):
|
||||
logger.debug("Non-user install because site-packages writeable")
|
||||
return False
|
||||
|
||||
logger.info("Defaulting to user installation because normal site-packages "
|
||||
"is not writeable")
|
||||
return True
|
||||
|
||||
|
||||
def warn_deprecated_install_options(requirement_set, options):
|
||||
# type: (RequirementSet, Optional[List[str]]) -> None
|
||||
"""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):
|
||||
# type: (Iterable[str]) -> List[str]
|
||||
return ["--{}".format(name.replace("_", "-")) for name in option_names]
|
||||
|
||||
requirements = (
|
||||
requirement_set.unnamed_requirements +
|
||||
list(requirement_set.requirements.values())
|
||||
)
|
||||
|
||||
offenders = []
|
||||
|
||||
for requirement in requirements:
|
||||
install_options = requirement.options.get("install_options", [])
|
||||
location_options = parse_distutils_args(install_options)
|
||||
if location_options:
|
||||
offenders.append(
|
||||
"{!r} from {}".format(
|
||||
format_options(location_options.keys()), requirement
|
||||
)
|
||||
)
|
||||
|
||||
if options:
|
||||
location_options = parse_distutils_args(options)
|
||||
if location_options:
|
||||
offenders.append(
|
||||
"{!r} from command line".format(
|
||||
format_options(location_options.keys())
|
||||
)
|
||||
)
|
||||
|
||||
if not offenders:
|
||||
return
|
||||
|
||||
deprecated(
|
||||
reason=(
|
||||
"Location-changing options found in --install-option: {}. "
|
||||
"This configuration may cause unexpected behavior and is "
|
||||
"unsupported.".format(
|
||||
"; ".join(offenders)
|
||||
)
|
||||
),
|
||||
replacement=(
|
||||
"using pip-level options like --user, --prefix, --root, and "
|
||||
"--target"
|
||||
),
|
||||
gone_in="20.2",
|
||||
issue=7309,
|
||||
)
|
||||
|
||||
|
||||
def create_env_error_message(error, show_traceback, using_user_site):
|
||||
"""Format an error message for an EnvironmentError
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ from pipenv.patched.notpip._vendor.six.moves import zip_longest
|
||||
from pipenv.patched.notpip._internal.cli import cmdoptions
|
||||
from pipenv.patched.notpip._internal.cli.req_command import IndexGroupCommand
|
||||
from pipenv.patched.notpip._internal.exceptions import CommandError
|
||||
from pipenv.patched.notpip._internal.index import PackageFinder
|
||||
from pipenv.patched.notpip._internal.index.package_finder import PackageFinder
|
||||
from pipenv.patched.notpip._internal.models.selection_prefs import SelectionPreferences
|
||||
from pipenv.patched.notpip._internal.self_outdated_check import make_link_collector
|
||||
from pipenv.patched.notpip._internal.utils.misc import (
|
||||
|
||||
@@ -7,16 +7,18 @@ from __future__ import absolute_import
|
||||
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
|
||||
from pipenv.patched.notpip._internal.cache import WheelCache
|
||||
from pipenv.patched.notpip._internal.cli import cmdoptions
|
||||
from pipenv.patched.notpip._internal.cli.req_command import RequirementCommand
|
||||
from pipenv.patched.notpip._internal.exceptions import CommandError, PreviousBuildDirError
|
||||
from pipenv.patched.notpip._internal.req import RequirementSet
|
||||
from pipenv.patched.notpip._internal.req.req_tracker import RequirementTracker
|
||||
from pipenv.patched.notpip._internal.req.req_tracker import get_requirement_tracker
|
||||
from pipenv.patched.notpip._internal.utils.misc import ensure_dir, normalize_path
|
||||
from pipenv.patched.notpip._internal.utils.temp_dir import TempDirectory
|
||||
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
|
||||
from pipenv.patched.notpip._internal.wheel import WheelBuilder
|
||||
from pipenv.patched.notpip._internal.wheel_builder import build, should_build_for_wheel_command
|
||||
|
||||
if MYPY_CHECK_RUNNING:
|
||||
from optparse import Values
|
||||
@@ -114,24 +116,20 @@ class WheelCommand(RequirementCommand):
|
||||
# type: (Values, List[Any]) -> None
|
||||
cmdoptions.check_install_build_global(options)
|
||||
|
||||
if options.build_dir:
|
||||
options.build_dir = os.path.abspath(options.build_dir)
|
||||
|
||||
options.src_dir = os.path.abspath(options.src_dir)
|
||||
|
||||
session = self.get_default_session(options)
|
||||
|
||||
finder = self._build_package_finder(options, session)
|
||||
build_delete = (not (options.no_clean or options.build_dir))
|
||||
wheel_cache = WheelCache(options.cache_dir, options.format_control)
|
||||
|
||||
with RequirementTracker() as req_tracker, TempDirectory(
|
||||
options.wheel_dir = normalize_path(options.wheel_dir)
|
||||
ensure_dir(options.wheel_dir)
|
||||
|
||||
with get_requirement_tracker() as req_tracker, TempDirectory(
|
||||
options.build_dir, delete=build_delete, kind="wheel"
|
||||
) as directory:
|
||||
|
||||
requirement_set = RequirementSet(
|
||||
require_hashes=options.require_hashes,
|
||||
)
|
||||
requirement_set = RequirementSet()
|
||||
|
||||
try:
|
||||
self.populate_requirement_set(
|
||||
@@ -143,30 +141,49 @@ class WheelCommand(RequirementCommand):
|
||||
temp_build_dir=directory,
|
||||
options=options,
|
||||
req_tracker=req_tracker,
|
||||
session=session,
|
||||
finder=finder,
|
||||
wheel_download_dir=options.wheel_dir,
|
||||
use_user_site=False,
|
||||
)
|
||||
|
||||
resolver = self.make_resolver(
|
||||
preparer=preparer,
|
||||
finder=finder,
|
||||
session=session,
|
||||
options=options,
|
||||
wheel_cache=wheel_cache,
|
||||
ignore_requires_python=options.ignore_requires_python,
|
||||
use_pep517=options.use_pep517,
|
||||
)
|
||||
|
||||
self.trace_basic_info(finder)
|
||||
|
||||
resolver.resolve(requirement_set)
|
||||
|
||||
reqs_to_build = [
|
||||
r for r in requirement_set.requirements.values()
|
||||
if should_build_for_wheel_command(r)
|
||||
]
|
||||
|
||||
# build wheels
|
||||
wb = WheelBuilder(
|
||||
preparer, wheel_cache,
|
||||
build_successes, build_failures = build(
|
||||
reqs_to_build,
|
||||
wheel_cache=wheel_cache,
|
||||
build_options=options.build_options or [],
|
||||
global_options=options.global_options or [],
|
||||
no_clean=options.no_clean,
|
||||
)
|
||||
build_failures = wb.build(
|
||||
requirement_set.requirements.values(),
|
||||
)
|
||||
for req in build_successes:
|
||||
assert req.link and req.link.is_wheel
|
||||
assert req.local_file_path
|
||||
# copy from cache to target directory
|
||||
try:
|
||||
shutil.copy(req.local_file_path, options.wheel_dir)
|
||||
except OSError as e:
|
||||
logger.warning(
|
||||
"Building wheel for %s failed: %s",
|
||||
req.name, e,
|
||||
)
|
||||
build_failures.append(req)
|
||||
if len(build_failures) != 0:
|
||||
raise CommandError(
|
||||
"Failed to build one or more wheels"
|
||||
|
||||
@@ -13,7 +13,6 @@ Some terminology:
|
||||
|
||||
# The following comment should be removed at some point in the future.
|
||||
# mypy: strict-optional=False
|
||||
# mypy: disallow-untyped-defs=False
|
||||
|
||||
import locale
|
||||
import logging
|
||||
@@ -78,6 +77,7 @@ CONFIG_BASENAME = 'pip.ini' if WINDOWS else 'pip.conf'
|
||||
|
||||
|
||||
def get_configuration_files():
|
||||
# type: () -> Dict[Kind, List[str]]
|
||||
global_config_files = [
|
||||
os.path.join(path, CONFIG_BASENAME)
|
||||
for path in appdirs.site_config_dirs('pip')
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from pipenv.patched.notpip._internal.distributions.source.legacy import SourceDistribution
|
||||
from pipenv.patched.notpip._internal.distributions.sdist import SourceDistribution
|
||||
from pipenv.patched.notpip._internal.distributions.wheel import WheelDistribution
|
||||
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
|
||||
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
# The following comment should be removed at some point in the future.
|
||||
# mypy: disallow-untyped-defs=False
|
||||
|
||||
import abc
|
||||
|
||||
from pipenv.patched.notpip._vendor.six import add_metaclass
|
||||
|
||||
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
|
||||
|
||||
if MYPY_CHECK_RUNNING:
|
||||
from typing import Optional
|
||||
|
||||
from pipenv.patched.notpip._vendor.pkg_resources import Distribution
|
||||
from pipenv.patched.notpip._internal.req import InstallRequirement
|
||||
from pipenv.patched.notpip._internal.index.package_finder import PackageFinder
|
||||
|
||||
|
||||
@add_metaclass(abc.ABCMeta)
|
||||
class AbstractDistribution(object):
|
||||
@@ -24,13 +30,16 @@ class AbstractDistribution(object):
|
||||
"""
|
||||
|
||||
def __init__(self, req):
|
||||
# type: (InstallRequirement) -> None
|
||||
super(AbstractDistribution, self).__init__()
|
||||
self.req = req
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_pkg_resources_distribution(self):
|
||||
# type: () -> Optional[Distribution]
|
||||
raise NotImplementedError()
|
||||
|
||||
@abc.abstractmethod
|
||||
def prepare_distribution_metadata(self, finder, build_isolation):
|
||||
# type: (PackageFinder, bool) -> None
|
||||
raise NotImplementedError()
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
# The following comment should be removed at some point in the future.
|
||||
# mypy: disallow-untyped-defs=False
|
||||
|
||||
from pipenv.patched.notpip._internal.distributions.base import AbstractDistribution
|
||||
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
|
||||
|
||||
if MYPY_CHECK_RUNNING:
|
||||
from typing import Optional
|
||||
|
||||
from pipenv.patched.notpip._vendor.pkg_resources import Distribution
|
||||
from pipenv.patched.notpip._internal.index.package_finder import PackageFinder
|
||||
|
||||
|
||||
class InstalledDistribution(AbstractDistribution):
|
||||
@@ -12,7 +16,9 @@ class InstalledDistribution(AbstractDistribution):
|
||||
"""
|
||||
|
||||
def get_pkg_resources_distribution(self):
|
||||
# type: () -> Optional[Distribution]
|
||||
return self.req.satisfied_by
|
||||
|
||||
def prepare_distribution_metadata(self, finder, build_isolation):
|
||||
# type: (PackageFinder, bool) -> None
|
||||
pass
|
||||
|
||||
+21
-15
@@ -1,12 +1,17 @@
|
||||
# The following comment should be removed at some point in the future.
|
||||
# mypy: disallow-untyped-defs=False
|
||||
|
||||
import logging
|
||||
|
||||
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.utils.subprocess import runner_with_spinner_message
|
||||
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
|
||||
|
||||
if MYPY_CHECK_RUNNING:
|
||||
from typing import Set, Tuple
|
||||
|
||||
from pipenv.patched.notpip._vendor.pkg_resources import Distribution
|
||||
from pipenv.patched.notpip._internal.index.package_finder import PackageFinder
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -16,31 +21,28 @@ class SourceDistribution(AbstractDistribution):
|
||||
|
||||
The preparation step for these needs metadata for the packages to be
|
||||
generated, either using PEP 517 or using the legacy `setup.py egg_info`.
|
||||
|
||||
NOTE from @pradyunsg (14 June 2019)
|
||||
I expect SourceDistribution class will need to be split into
|
||||
`legacy_source` (setup.py based) and `source` (PEP 517 based) when we start
|
||||
bringing logic for preparation out of InstallRequirement into this class.
|
||||
"""
|
||||
|
||||
def get_pkg_resources_distribution(self):
|
||||
# type: () -> Distribution
|
||||
return self.req.get_dist()
|
||||
|
||||
def prepare_distribution_metadata(self, finder, build_isolation):
|
||||
# Prepare for building. We need to:
|
||||
# 1. Load pyproject.toml (if it exists)
|
||||
# 2. Set up the build environment
|
||||
|
||||
# type: (PackageFinder, bool) -> None
|
||||
# Load pyproject.toml, to determine whether PEP 517 is to be used
|
||||
self.req.load_pyproject_toml()
|
||||
|
||||
# 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)
|
||||
|
||||
self.req.prepare_metadata()
|
||||
self.req.assert_source_matches_version()
|
||||
|
||||
def _setup_isolation(self, finder):
|
||||
# type: (PackageFinder) -> None
|
||||
def _raise_conflicts(conflicting_with, conflicting_reqs):
|
||||
# type: (str, Set[Tuple[str, str]]) -> None
|
||||
format_string = (
|
||||
"Some build dependencies for {requirement} "
|
||||
"conflict with {conflicting_with}: {description}."
|
||||
@@ -49,7 +51,7 @@ class SourceDistribution(AbstractDistribution):
|
||||
requirement=self.req,
|
||||
conflicting_with=conflicting_with,
|
||||
description=', '.join(
|
||||
'%s is incompatible with %s' % (installed, wanted)
|
||||
'{} is incompatible with {}'.format(installed, wanted)
|
||||
for installed, wanted in sorted(conflicting)
|
||||
)
|
||||
)
|
||||
@@ -57,9 +59,12 @@ class SourceDistribution(AbstractDistribution):
|
||||
|
||||
# Isolate in a BuildEnvironment and install the build-time
|
||||
# requirements.
|
||||
pyproject_requires = self.req.pyproject_requires
|
||||
assert pyproject_requires is not None
|
||||
|
||||
self.req.build_env = BuildEnvironment()
|
||||
self.req.build_env.install_requirements(
|
||||
finder, self.req.pyproject_requires, 'overlay',
|
||||
finder, pyproject_requires, 'overlay',
|
||||
"Installing build dependencies"
|
||||
)
|
||||
conflicting, missing = self.req.build_env.check_requirements(
|
||||
@@ -86,6 +91,7 @@ class SourceDistribution(AbstractDistribution):
|
||||
"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()
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
# The following comment should be removed at some point in the future.
|
||||
# mypy: disallow-untyped-defs=False
|
||||
|
||||
from pipenv.patched.notpip._vendor import pkg_resources
|
||||
from zipfile import ZipFile
|
||||
|
||||
from pipenv.patched.notpip._internal.distributions.base import AbstractDistribution
|
||||
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
|
||||
from pipenv.patched.notpip._internal.utils.wheel import pkg_resources_distribution_for_wheel
|
||||
|
||||
if MYPY_CHECK_RUNNING:
|
||||
from pipenv.patched.notpip._vendor.pkg_resources import Distribution
|
||||
from pipenv.patched.notpip._internal.index.package_finder import PackageFinder
|
||||
|
||||
|
||||
class WheelDistribution(AbstractDistribution):
|
||||
@@ -13,8 +16,21 @@ class WheelDistribution(AbstractDistribution):
|
||||
"""
|
||||
|
||||
def get_pkg_resources_distribution(self):
|
||||
return list(pkg_resources.find_distributions(
|
||||
self.req.source_dir))[0]
|
||||
# type: () -> Distribution
|
||||
"""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
|
||||
)
|
||||
|
||||
def prepare_distribution_metadata(self, finder, build_isolation):
|
||||
# type: (PackageFinder, bool) -> None
|
||||
pass
|
||||
|
||||
@@ -1,578 +0,0 @@
|
||||
# The following comment should be removed at some point in the future.
|
||||
# mypy: disallow-untyped-defs=False
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import cgi
|
||||
import logging
|
||||
import mimetypes
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
from pipenv.patched.notpip._vendor import requests
|
||||
from pipenv.patched.notpip._vendor.requests.models import CONTENT_CHUNK_SIZE, Response
|
||||
from pipenv.patched.notpip._vendor.six import PY2
|
||||
from pipenv.patched.notpip._vendor.six.moves.urllib import parse as urllib_parse
|
||||
|
||||
from pipenv.patched.notpip._internal.exceptions import HashMismatch, InstallationError
|
||||
from pipenv.patched.notpip._internal.models.index import PyPI
|
||||
from pipenv.patched.notpip._internal.network.session import PipSession
|
||||
from pipenv.patched.notpip._internal.utils.encoding import auto_decode
|
||||
from pipenv.patched.notpip._internal.utils.filesystem import copy2_fixed
|
||||
from pipenv.patched.notpip._internal.utils.misc import (
|
||||
ask_path_exists,
|
||||
backup_dir,
|
||||
consume,
|
||||
display_path,
|
||||
format_size,
|
||||
hide_url,
|
||||
path_to_display,
|
||||
rmtree,
|
||||
splitext,
|
||||
)
|
||||
from pipenv.patched.notpip._internal.utils.temp_dir import TempDirectory
|
||||
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
|
||||
from pipenv.patched.notpip._internal.utils.ui import DownloadProgressProvider
|
||||
from pipenv.patched.notpip._internal.utils.unpacking import unpack_file
|
||||
from pipenv.patched.notpip._internal.utils.urls import get_url_scheme
|
||||
from pipenv.patched.notpip._internal.vcs import vcs
|
||||
|
||||
if MYPY_CHECK_RUNNING:
|
||||
from typing import (
|
||||
IO, Callable, List, Optional, Text, Tuple,
|
||||
)
|
||||
|
||||
from mypy_extensions import TypedDict
|
||||
|
||||
from pipenv.patched.notpip._internal.models.link import Link
|
||||
from pipenv.patched.notpip._internal.utils.hashes import Hashes
|
||||
from pipenv.patched.notpip._internal.vcs.versioncontrol import VersionControl
|
||||
|
||||
if PY2:
|
||||
CopytreeKwargs = TypedDict(
|
||||
'CopytreeKwargs',
|
||||
{
|
||||
'ignore': Callable[[str, List[str]], List[str]],
|
||||
'symlinks': bool,
|
||||
},
|
||||
total=False,
|
||||
)
|
||||
else:
|
||||
CopytreeKwargs = TypedDict(
|
||||
'CopytreeKwargs',
|
||||
{
|
||||
'copy_function': Callable[[str, str], None],
|
||||
'ignore': Callable[[str, List[str]], List[str]],
|
||||
'ignore_dangling_symlinks': bool,
|
||||
'symlinks': bool,
|
||||
},
|
||||
total=False,
|
||||
)
|
||||
|
||||
|
||||
__all__ = ['get_file_content',
|
||||
'unpack_vcs_link',
|
||||
'unpack_file_url',
|
||||
'unpack_http_url', 'unpack_url',
|
||||
'parse_content_disposition', 'sanitize_content_filename']
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_file_content(url, comes_from=None, session=None):
|
||||
# type: (str, Optional[str], Optional[PipSession]) -> Tuple[str, Text]
|
||||
"""Gets the content of a file; it may be a filename, file: URL, or
|
||||
http: URL. Returns (location, content). Content is unicode.
|
||||
|
||||
:param url: File path or url.
|
||||
:param comes_from: Origin description of requirements.
|
||||
:param session: Instance of pip.download.PipSession.
|
||||
"""
|
||||
if session is None:
|
||||
raise TypeError(
|
||||
"get_file_content() missing 1 required keyword argument: 'session'"
|
||||
)
|
||||
|
||||
scheme = get_url_scheme(url)
|
||||
|
||||
if scheme in ['http', 'https']:
|
||||
# FIXME: catch some errors
|
||||
resp = session.get(url)
|
||||
resp.raise_for_status()
|
||||
return resp.url, resp.text
|
||||
|
||||
elif scheme == 'file':
|
||||
if comes_from and comes_from.startswith('http'):
|
||||
raise InstallationError(
|
||||
'Requirements file %s references URL %s, which is local'
|
||||
% (comes_from, url))
|
||||
|
||||
path = url.split(':', 1)[1]
|
||||
path = path.replace('\\', '/')
|
||||
match = _url_slash_drive_re.match(path)
|
||||
if match:
|
||||
path = match.group(1) + ':' + path.split('|', 1)[1]
|
||||
path = urllib_parse.unquote(path)
|
||||
if path.startswith('/'):
|
||||
path = '/' + path.lstrip('/')
|
||||
url = path
|
||||
|
||||
try:
|
||||
with open(url, 'rb') as f:
|
||||
content = auto_decode(f.read())
|
||||
except IOError as exc:
|
||||
raise InstallationError(
|
||||
'Could not open requirements file: %s' % str(exc)
|
||||
)
|
||||
return url, content
|
||||
|
||||
|
||||
_url_slash_drive_re = re.compile(r'/*([a-z])\|', re.I)
|
||||
|
||||
|
||||
def unpack_vcs_link(link, location):
|
||||
# type: (Link, str) -> None
|
||||
vcs_backend = _get_used_vcs_backend(link)
|
||||
assert vcs_backend is not None
|
||||
vcs_backend.unpack(location, url=hide_url(link.url))
|
||||
|
||||
|
||||
def _get_used_vcs_backend(link):
|
||||
# type: (Link) -> Optional[VersionControl]
|
||||
"""
|
||||
Return a VersionControl object or None.
|
||||
"""
|
||||
for vcs_backend in vcs.backends:
|
||||
if link.scheme in vcs_backend.schemes:
|
||||
return vcs_backend
|
||||
return None
|
||||
|
||||
|
||||
def _progress_indicator(iterable, *args, **kwargs):
|
||||
return iterable
|
||||
|
||||
|
||||
def _download_url(
|
||||
resp, # type: Response
|
||||
link, # type: Link
|
||||
content_file, # type: IO
|
||||
hashes, # type: Optional[Hashes]
|
||||
progress_bar # type: str
|
||||
):
|
||||
# type: (...) -> None
|
||||
try:
|
||||
total_length = int(resp.headers['content-length'])
|
||||
except (ValueError, KeyError, TypeError):
|
||||
total_length = 0
|
||||
|
||||
cached_resp = getattr(resp, "from_cache", False)
|
||||
if logger.getEffectiveLevel() > logging.INFO:
|
||||
show_progress = False
|
||||
elif cached_resp:
|
||||
show_progress = False
|
||||
elif total_length > (40 * 1000):
|
||||
show_progress = True
|
||||
elif not total_length:
|
||||
show_progress = True
|
||||
else:
|
||||
show_progress = False
|
||||
|
||||
show_url = link.show_url
|
||||
|
||||
def resp_read(chunk_size):
|
||||
try:
|
||||
# Special case for urllib3.
|
||||
for chunk in resp.raw.stream(
|
||||
chunk_size,
|
||||
# We use decode_content=False here because we don't
|
||||
# want urllib3 to mess with the raw bytes we get
|
||||
# from the server. If we decompress inside of
|
||||
# urllib3 then we cannot verify the checksum
|
||||
# because the checksum will be of the compressed
|
||||
# file. This breakage will only occur if the
|
||||
# server adds a Content-Encoding header, which
|
||||
# depends on how the server was configured:
|
||||
# - Some servers will notice that the file isn't a
|
||||
# compressible file and will leave the file alone
|
||||
# and with an empty Content-Encoding
|
||||
# - Some servers will notice that the file is
|
||||
# already compressed and will leave the file
|
||||
# alone and will add a Content-Encoding: gzip
|
||||
# header
|
||||
# - Some servers won't notice anything at all and
|
||||
# will take a file that's already been compressed
|
||||
# and compress it again and set the
|
||||
# Content-Encoding: gzip header
|
||||
#
|
||||
# By setting this not to decode automatically we
|
||||
# hope to eliminate problems with the second case.
|
||||
decode_content=False):
|
||||
yield chunk
|
||||
except AttributeError:
|
||||
# Standard file-like object.
|
||||
while True:
|
||||
chunk = resp.raw.read(chunk_size)
|
||||
if not chunk:
|
||||
break
|
||||
yield chunk
|
||||
|
||||
def written_chunks(chunks):
|
||||
for chunk in chunks:
|
||||
content_file.write(chunk)
|
||||
yield chunk
|
||||
|
||||
progress_indicator = _progress_indicator
|
||||
|
||||
if link.netloc == PyPI.netloc:
|
||||
url = show_url
|
||||
else:
|
||||
url = link.url_without_fragment
|
||||
|
||||
if show_progress: # We don't show progress on cached responses
|
||||
progress_indicator = DownloadProgressProvider(progress_bar,
|
||||
max=total_length)
|
||||
if total_length:
|
||||
logger.info("Downloading %s (%s)", url, format_size(total_length))
|
||||
else:
|
||||
logger.info("Downloading %s", url)
|
||||
elif cached_resp:
|
||||
logger.info("Using cached %s", url)
|
||||
else:
|
||||
logger.info("Downloading %s", url)
|
||||
|
||||
downloaded_chunks = written_chunks(
|
||||
progress_indicator(
|
||||
resp_read(CONTENT_CHUNK_SIZE),
|
||||
CONTENT_CHUNK_SIZE
|
||||
)
|
||||
)
|
||||
if hashes:
|
||||
hashes.check_against_chunks(downloaded_chunks)
|
||||
else:
|
||||
consume(downloaded_chunks)
|
||||
|
||||
|
||||
def _copy_file(filename, location, link):
|
||||
copy = True
|
||||
download_location = os.path.join(location, link.filename)
|
||||
if os.path.exists(download_location):
|
||||
response = ask_path_exists(
|
||||
'The file %s exists. (i)gnore, (w)ipe, (b)ackup, (a)abort' %
|
||||
display_path(download_location), ('i', 'w', 'b', 'a'))
|
||||
if response == 'i':
|
||||
copy = False
|
||||
elif response == 'w':
|
||||
logger.warning('Deleting %s', display_path(download_location))
|
||||
os.remove(download_location)
|
||||
elif response == 'b':
|
||||
dest_file = backup_dir(download_location)
|
||||
logger.warning(
|
||||
'Backing up %s to %s',
|
||||
display_path(download_location),
|
||||
display_path(dest_file),
|
||||
)
|
||||
shutil.move(download_location, dest_file)
|
||||
elif response == 'a':
|
||||
sys.exit(-1)
|
||||
if copy:
|
||||
shutil.copy(filename, download_location)
|
||||
logger.info('Saved %s', display_path(download_location))
|
||||
|
||||
|
||||
def unpack_http_url(
|
||||
link, # type: Link
|
||||
location, # type: str
|
||||
download_dir=None, # type: Optional[str]
|
||||
session=None, # type: Optional[PipSession]
|
||||
hashes=None, # type: Optional[Hashes]
|
||||
progress_bar="on" # type: str
|
||||
):
|
||||
# type: (...) -> None
|
||||
if session is None:
|
||||
raise TypeError(
|
||||
"unpack_http_url() missing 1 required keyword argument: 'session'"
|
||||
)
|
||||
|
||||
with TempDirectory(kind="unpack") as temp_dir:
|
||||
# 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)
|
||||
|
||||
if already_downloaded_path:
|
||||
from_path = already_downloaded_path
|
||||
content_type = mimetypes.guess_type(from_path)[0]
|
||||
else:
|
||||
# let's download to a tmp dir
|
||||
from_path, content_type = _download_http_url(link,
|
||||
session,
|
||||
temp_dir.path,
|
||||
hashes,
|
||||
progress_bar)
|
||||
|
||||
# unpack the archive to the build dir location. even when only
|
||||
# downloading archives, they have to be unpacked to parse dependencies
|
||||
unpack_file(from_path, location, content_type)
|
||||
|
||||
# a download dir is specified; let's copy the archive there
|
||||
if download_dir and not already_downloaded_path:
|
||||
_copy_file(from_path, download_dir, link)
|
||||
|
||||
if not already_downloaded_path:
|
||||
os.unlink(from_path)
|
||||
|
||||
|
||||
def _copy2_ignoring_special_files(src, dest):
|
||||
# type: (str, 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.
|
||||
"""
|
||||
try:
|
||||
copy2_fixed(src, dest)
|
||||
except shutil.SpecialFileError as e:
|
||||
# SpecialFileError may be raised due to either the source or
|
||||
# destination. If the destination was the cause then we would actually
|
||||
# care, but since the destination directory is deleted prior to
|
||||
# copy we ignore all of them assuming it is caused by the source.
|
||||
logger.warning(
|
||||
"Ignoring special file error '%s' encountered copying %s to %s.",
|
||||
str(e),
|
||||
path_to_display(src),
|
||||
path_to_display(dest),
|
||||
)
|
||||
|
||||
|
||||
def _copy_source_tree(source, target):
|
||||
# type: (str, str) -> None
|
||||
def ignore(d, names):
|
||||
# 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
|
||||
return ['.tox', '.nox'] if d == source else []
|
||||
|
||||
kwargs = dict(ignore=ignore, symlinks=True) # type: CopytreeKwargs
|
||||
|
||||
if not PY2:
|
||||
# Python 2 does not support copy_function, so we only ignore
|
||||
# errors on special file copy in Python 3.
|
||||
kwargs['copy_function'] = _copy2_ignoring_special_files
|
||||
|
||||
shutil.copytree(source, target, **kwargs)
|
||||
|
||||
|
||||
def unpack_file_url(
|
||||
link, # type: Link
|
||||
location, # type: str
|
||||
download_dir=None, # type: Optional[str]
|
||||
hashes=None # type: Optional[Hashes]
|
||||
):
|
||||
# type: (...) -> None
|
||||
"""Unpack link into location.
|
||||
|
||||
If download_dir is provided and link points to a file, make a copy
|
||||
of the link file inside download_dir.
|
||||
"""
|
||||
link_path = link.file_path
|
||||
# If it's a url to a local directory
|
||||
if link.is_existing_dir():
|
||||
if os.path.isdir(location):
|
||||
rmtree(location)
|
||||
_copy_source_tree(link_path, location)
|
||||
if download_dir:
|
||||
logger.info('Link is a directory, ignoring download_dir')
|
||||
return
|
||||
|
||||
# If --require-hashes is off, `hashes` is either empty, the
|
||||
# link's embedded hash, or MissingHashes; it is required to
|
||||
# match. If --require-hashes is on, we are satisfied by any
|
||||
# hash in `hashes` matching: a URL-based or an option-based
|
||||
# one; no internet-sourced hash will be in `hashes`.
|
||||
if hashes:
|
||||
hashes.check_against_path(link_path)
|
||||
|
||||
# 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)
|
||||
|
||||
if already_downloaded_path:
|
||||
from_path = already_downloaded_path
|
||||
else:
|
||||
from_path = link_path
|
||||
|
||||
content_type = mimetypes.guess_type(from_path)[0]
|
||||
|
||||
# unpack the archive to the build dir location. even when only downloading
|
||||
# archives, they have to be unpacked to parse dependencies
|
||||
unpack_file(from_path, location, content_type)
|
||||
|
||||
# a download dir is specified and not already downloaded
|
||||
if download_dir and not already_downloaded_path:
|
||||
_copy_file(from_path, download_dir, link)
|
||||
|
||||
|
||||
def unpack_url(
|
||||
link, # type: Link
|
||||
location, # type: str
|
||||
download_dir=None, # type: Optional[str]
|
||||
session=None, # type: Optional[PipSession]
|
||||
hashes=None, # type: Optional[Hashes]
|
||||
progress_bar="on" # type: str
|
||||
):
|
||||
# type: (...) -> None
|
||||
"""Unpack link.
|
||||
If link is a VCS link:
|
||||
if only_download, export into download_dir and ignore location
|
||||
else unpack into location
|
||||
for other types of link:
|
||||
- unpack into location
|
||||
- if download_dir, copy the file into download_dir
|
||||
- if only_download, mark location for deletion
|
||||
|
||||
:param hashes: A Hashes object, one of whose embedded hashes must match,
|
||||
or HashMismatch will be raised. If the Hashes is empty, no matches are
|
||||
required, and unhashable types of requirements (like VCS ones, which
|
||||
would ordinarily raise HashUnsupported) are allowed.
|
||||
"""
|
||||
# non-editable vcs urls
|
||||
if link.is_vcs:
|
||||
unpack_vcs_link(link, location)
|
||||
|
||||
# file urls
|
||||
elif link.is_file:
|
||||
unpack_file_url(link, location, download_dir, hashes=hashes)
|
||||
|
||||
# http urls
|
||||
else:
|
||||
if session is None:
|
||||
session = PipSession()
|
||||
|
||||
unpack_http_url(
|
||||
link,
|
||||
location,
|
||||
download_dir,
|
||||
session,
|
||||
hashes=hashes,
|
||||
progress_bar=progress_bar
|
||||
)
|
||||
|
||||
|
||||
def sanitize_content_filename(filename):
|
||||
# type: (str) -> str
|
||||
"""
|
||||
Sanitize the "filename" value from a Content-Disposition header.
|
||||
"""
|
||||
return os.path.basename(filename)
|
||||
|
||||
|
||||
def parse_content_disposition(content_disposition, default_filename):
|
||||
# type: (str, str) -> str
|
||||
"""
|
||||
Parse the "filename" value from a Content-Disposition header, and
|
||||
return the default filename if the result is empty.
|
||||
"""
|
||||
_type, params = cgi.parse_header(content_disposition)
|
||||
filename = params.get('filename')
|
||||
if filename:
|
||||
# We need to sanitize the filename to prevent directory traversal
|
||||
# in case the filename contains ".." path parts.
|
||||
filename = sanitize_content_filename(filename)
|
||||
return filename or default_filename
|
||||
|
||||
|
||||
def _download_http_url(
|
||||
link, # type: Link
|
||||
session, # type: PipSession
|
||||
temp_dir, # type: str
|
||||
hashes, # type: Optional[Hashes]
|
||||
progress_bar # type: str
|
||||
):
|
||||
# type: (...) -> Tuple[str, str]
|
||||
"""Download link url into temp_dir using provided session"""
|
||||
target_url = link.url.split('#', 1)[0]
|
||||
try:
|
||||
resp = session.get(
|
||||
target_url,
|
||||
# We use Accept-Encoding: identity here because requests
|
||||
# defaults to accepting compressed responses. This breaks in
|
||||
# a variety of ways depending on how the server is configured.
|
||||
# - Some servers will notice that the file isn't a compressible
|
||||
# file and will leave the file alone and with an empty
|
||||
# Content-Encoding
|
||||
# - Some servers will notice that the file is already
|
||||
# compressed and will leave the file alone and will add a
|
||||
# Content-Encoding: gzip header
|
||||
# - Some servers won't notice anything at all and will take
|
||||
# a file that's already been compressed and compress it again
|
||||
# and set the Content-Encoding: gzip header
|
||||
# By setting this to request only the identity encoding We're
|
||||
# hoping to eliminate the third case. Hopefully there does not
|
||||
# exist a server which when given a file will notice it is
|
||||
# already compressed and that you're not asking for a
|
||||
# compressed file and will then decompress it before sending
|
||||
# because if that's the case I don't think it'll ever be
|
||||
# possible to make this work.
|
||||
headers={"Accept-Encoding": "identity"},
|
||||
stream=True,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
except requests.HTTPError as exc:
|
||||
logger.critical(
|
||||
"HTTP error %s while getting %s", exc.response.status_code, link,
|
||||
)
|
||||
raise
|
||||
|
||||
content_type = resp.headers.get('content-type', '')
|
||||
filename = link.filename # fallback
|
||||
# Have a look at the Content-Disposition header for a better guess
|
||||
content_disposition = resp.headers.get('content-disposition')
|
||||
if content_disposition:
|
||||
filename = parse_content_disposition(content_disposition, filename)
|
||||
ext = splitext(filename)[1] # type: Optional[str]
|
||||
if not ext:
|
||||
ext = mimetypes.guess_extension(content_type)
|
||||
if ext:
|
||||
filename += ext
|
||||
if not ext and link.url != resp.url:
|
||||
ext = os.path.splitext(resp.url)[1]
|
||||
if ext:
|
||||
filename += ext
|
||||
file_path = os.path.join(temp_dir, filename)
|
||||
with open(file_path, 'wb') as content_file:
|
||||
_download_url(resp, link, content_file, hashes, progress_bar)
|
||||
return file_path, content_type
|
||||
|
||||
|
||||
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
|
||||
"""
|
||||
download_path = os.path.join(download_dir, link.filename)
|
||||
|
||||
if not os.path.exists(download_path):
|
||||
return None
|
||||
|
||||
# If already downloaded, does its hash match?
|
||||
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
|
||||
)
|
||||
os.unlink(download_path)
|
||||
return None
|
||||
return download_path
|
||||
@@ -0,0 +1,2 @@
|
||||
"""Index interaction code
|
||||
"""
|
||||
+24
-28
@@ -2,9 +2,6 @@
|
||||
The main purpose of this module is to expose LinkCollector.collect_links().
|
||||
"""
|
||||
|
||||
# The following comment should be removed at some point in the future.
|
||||
# mypy: disallow-untyped-defs=False
|
||||
|
||||
import cgi
|
||||
import itertools
|
||||
import logging
|
||||
@@ -27,8 +24,8 @@ from pipenv.patched.notpip._internal.vcs import is_url, vcs
|
||||
|
||||
if MYPY_CHECK_RUNNING:
|
||||
from typing import (
|
||||
Callable, Dict, Iterable, List, MutableMapping, Optional, Sequence,
|
||||
Tuple, Union,
|
||||
Callable, Iterable, List, MutableMapping, Optional, Sequence, Tuple,
|
||||
Union,
|
||||
)
|
||||
import xml.etree.ElementTree
|
||||
|
||||
@@ -290,6 +287,7 @@ class HTMLPage(object):
|
||||
self.url = url
|
||||
|
||||
def __str__(self):
|
||||
# type: () -> str
|
||||
return redact_auth_from_url(self.url)
|
||||
|
||||
|
||||
@@ -385,6 +383,7 @@ def group_locations(locations, expand_dir=False):
|
||||
|
||||
# puts the url for the given file path into the appropriate list
|
||||
def sort_path(path):
|
||||
# type: (str) -> None
|
||||
url = path_to_url(path)
|
||||
if mimetypes.guess_type(url, strict=False)[0] == 'text/html':
|
||||
urls.append(url)
|
||||
@@ -435,29 +434,36 @@ def group_locations(locations, expand_dir=False):
|
||||
class CollectedLinks(object):
|
||||
|
||||
"""
|
||||
Encapsulates all the Link objects collected by a call to
|
||||
LinkCollector.collect_links(), stored separately as--
|
||||
Encapsulates the return value of a call to LinkCollector.collect_links().
|
||||
|
||||
The return value includes both URLs to project pages containing package
|
||||
links, as well as individual package Link objects collected from other
|
||||
sources.
|
||||
|
||||
This info is stored separately as:
|
||||
|
||||
(1) links from the configured file locations,
|
||||
(2) links from the configured find_links, and
|
||||
(3) a dict mapping HTML page url to links from that page.
|
||||
(3) urls to HTML project pages, as described by the PEP 503 simple
|
||||
repository API.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
files, # type: List[Link]
|
||||
find_links, # type: List[Link]
|
||||
pages, # type: Dict[str, List[Link]]
|
||||
files, # type: List[Link]
|
||||
find_links, # type: List[Link]
|
||||
project_urls, # type: List[Link]
|
||||
):
|
||||
# type: (...) -> None
|
||||
"""
|
||||
:param files: Links from file locations.
|
||||
:param find_links: Links from find_links.
|
||||
:param pages: A dict mapping HTML page url to links from that page.
|
||||
:param project_urls: URLs to HTML project pages, as described by
|
||||
the PEP 503 simple repository API.
|
||||
"""
|
||||
self.files = files
|
||||
self.find_links = find_links
|
||||
self.pages = pages
|
||||
self.project_urls = project_urls
|
||||
|
||||
|
||||
class LinkCollector(object):
|
||||
@@ -483,18 +489,12 @@ class LinkCollector(object):
|
||||
# type: () -> List[str]
|
||||
return self.search_scope.find_links
|
||||
|
||||
def _get_pages(self, locations):
|
||||
# type: (Iterable[Link]) -> Iterable[HTMLPage]
|
||||
def fetch_page(self, location):
|
||||
# type: (Link) -> Optional[HTMLPage]
|
||||
"""
|
||||
Yields (page, page_url) from the given locations, skipping
|
||||
locations that have errors.
|
||||
Fetch an HTML page containing package links.
|
||||
"""
|
||||
for location in locations:
|
||||
page = _get_html_page(location, session=self.session)
|
||||
if page is None:
|
||||
continue
|
||||
|
||||
yield page
|
||||
return _get_html_page(location, session=self.session)
|
||||
|
||||
def collect_links(self, project_name):
|
||||
# type: (str) -> CollectedLinks
|
||||
@@ -537,12 +537,8 @@ class LinkCollector(object):
|
||||
lines.append('* {}'.format(link))
|
||||
logger.debug('\n'.join(lines))
|
||||
|
||||
pages_links = {}
|
||||
for page in self._get_pages(url_locations):
|
||||
pages_links[page.url] = list(parse_links(page))
|
||||
|
||||
return CollectedLinks(
|
||||
files=file_links,
|
||||
find_links=find_link_links,
|
||||
pages=pages_links,
|
||||
project_urls=url_locations,
|
||||
)
|
||||
+37
-16
@@ -2,7 +2,6 @@
|
||||
|
||||
# The following comment should be removed at some point in the future.
|
||||
# mypy: strict-optional=False
|
||||
# mypy: disallow-untyped-defs=False
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
@@ -19,11 +18,13 @@ from pipenv.patched.notpip._internal.exceptions import (
|
||||
InvalidWheelFilename,
|
||||
UnsupportedWheel,
|
||||
)
|
||||
from pipenv.patched.notpip._internal.index.collector import parse_links
|
||||
from pipenv.patched.notpip._internal.models.candidate import InstallationCandidate
|
||||
from pipenv.patched.notpip._internal.models.format_control import FormatControl
|
||||
from pipenv.patched.notpip._internal.models.link import Link
|
||||
from pipenv.patched.notpip._internal.models.selection_prefs import SelectionPreferences
|
||||
from pipenv.patched.notpip._internal.models.target_python import TargetPython
|
||||
from pipenv.patched.notpip._internal.models.wheel import Wheel
|
||||
from pipenv.patched.notpip._internal.utils.filetypes import WHEEL_EXTENSION
|
||||
from pipenv.patched.notpip._internal.utils.logging import indent_log
|
||||
from pipenv.patched.notpip._internal.utils.misc import build_netloc
|
||||
@@ -31,17 +32,18 @@ from pipenv.patched.notpip._internal.utils.packaging import check_requires_pytho
|
||||
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
|
||||
from pipenv.patched.notpip._internal.utils.unpacking import SUPPORTED_EXTENSIONS
|
||||
from pipenv.patched.notpip._internal.utils.urls import url_to_path
|
||||
from pipenv.patched.notpip._internal.wheel import Wheel
|
||||
|
||||
if MYPY_CHECK_RUNNING:
|
||||
from typing import (
|
||||
FrozenSet, Iterable, List, Optional, Set, Text, Tuple, Union,
|
||||
)
|
||||
|
||||
from pipenv.patched.notpip._vendor.packaging.tags import Tag
|
||||
from pipenv.patched.notpip._vendor.packaging.version import _BaseVersion
|
||||
from pipenv.patched.notpip._internal.collector import LinkCollector
|
||||
|
||||
from pipenv.patched.notpip._internal.index.collector import LinkCollector
|
||||
from pipenv.patched.notpip._internal.models.search_scope import SearchScope
|
||||
from pipenv.patched.notpip._internal.req import InstallRequirement
|
||||
from pipenv.patched.notpip._internal.pep425tags import Pep425Tag
|
||||
from pipenv.patched.notpip._internal.utils.hashes import Hashes
|
||||
|
||||
BuildTag = Union[Tuple[()], Tuple[int, str]]
|
||||
@@ -115,7 +117,7 @@ class LinkEvaluator(object):
|
||||
self,
|
||||
project_name, # type: str
|
||||
canonical_name, # type: str
|
||||
formats, # type: FrozenSet
|
||||
formats, # type: FrozenSet[str]
|
||||
target_python, # type: TargetPython
|
||||
allow_yanked, # type: bool
|
||||
ignore_requires_python=None, # type: Optional[bool]
|
||||
@@ -431,7 +433,7 @@ class CandidateEvaluator(object):
|
||||
def __init__(
|
||||
self,
|
||||
project_name, # type: str
|
||||
supported_tags, # type: List[Pep425Tag]
|
||||
supported_tags, # type: List[Tag]
|
||||
specifier, # type: specifiers.BaseSpecifier
|
||||
prefer_binary=False, # type: bool
|
||||
allow_all_prereleases=False, # type: bool
|
||||
@@ -479,12 +481,14 @@ class CandidateEvaluator(object):
|
||||
c for c in candidates if str(c.version) in versions
|
||||
]
|
||||
|
||||
return filter_unallowed_hashes(
|
||||
filtered_applicable_candidates = filter_unallowed_hashes(
|
||||
candidates=applicable_candidates,
|
||||
hashes=self._hashes,
|
||||
project_name=self._project_name,
|
||||
)
|
||||
|
||||
return sorted(filtered_applicable_candidates, key=self._sort_key)
|
||||
|
||||
def _sort_key(self, candidate, ignore_compatibility=True):
|
||||
# type: (InstallationCandidate, bool) -> CandidateSortingKey
|
||||
"""
|
||||
@@ -793,7 +797,7 @@ class PackageFinder(object):
|
||||
return None
|
||||
|
||||
return InstallationCandidate(
|
||||
project=link_evaluator.project_name,
|
||||
name=link_evaluator.project_name,
|
||||
link=link,
|
||||
# Convert the Text result to str since InstallationCandidate
|
||||
# accepts str.
|
||||
@@ -814,6 +818,25 @@ class PackageFinder(object):
|
||||
|
||||
return candidates
|
||||
|
||||
def process_project_url(self, project_url, link_evaluator):
|
||||
# type: (Link, LinkEvaluator) -> List[InstallationCandidate]
|
||||
logger.debug(
|
||||
'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))
|
||||
|
||||
with indent_log():
|
||||
package_links = self.evaluate_links(
|
||||
link_evaluator,
|
||||
links=page_links,
|
||||
)
|
||||
|
||||
return package_links
|
||||
|
||||
def find_all_candidates(self, project_name):
|
||||
# type: (str) -> List[InstallationCandidate]
|
||||
"""Find all available InstallationCandidate for project_name
|
||||
@@ -834,14 +857,11 @@ class PackageFinder(object):
|
||||
)
|
||||
|
||||
page_versions = []
|
||||
for page_url, page_links in collected_links.pages.items():
|
||||
logger.debug('Analyzing links from page %s', page_url)
|
||||
with indent_log():
|
||||
new_versions = self.evaluate_links(
|
||||
link_evaluator,
|
||||
links=page_links,
|
||||
)
|
||||
page_versions.extend(new_versions)
|
||||
for project_url in collected_links.project_urls:
|
||||
package_links = self.process_project_url(
|
||||
project_url, link_evaluator=link_evaluator,
|
||||
)
|
||||
page_versions.extend(package_links)
|
||||
|
||||
file_versions = self.evaluate_links(
|
||||
link_evaluator,
|
||||
@@ -921,6 +941,7 @@ class PackageFinder(object):
|
||||
installed_version = parse_version(req.satisfied_by.version)
|
||||
|
||||
def _format_versions(cand_iter):
|
||||
# type: (Iterable[InstallationCandidate]) -> str
|
||||
# This repeated parse_version and str() conversion is needed to
|
||||
# handle different vendoring sources from pipenv.patched.notpip and pkg_resources.
|
||||
# If we stop using the pkg_resources provided specifier and start
|
||||
@@ -29,11 +29,7 @@ from pipenv.patched.notpip._internal.exceptions import (
|
||||
UnsupportedPythonVersion,
|
||||
)
|
||||
from pipenv.patched.notpip._internal.utils.logging import indent_log
|
||||
from pipenv.patched.notpip._internal.utils.misc import (
|
||||
dist_in_usersite,
|
||||
ensure_dir,
|
||||
normalize_version_info,
|
||||
)
|
||||
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,
|
||||
@@ -45,8 +41,7 @@ if MYPY_CHECK_RUNNING:
|
||||
from pipenv.patched.notpip._vendor import pkg_resources
|
||||
|
||||
from pipenv.patched.notpip._internal.distributions import AbstractDistribution
|
||||
from pipenv.patched.notpip._internal.network.session import PipSession
|
||||
from pipenv.patched.notpip._internal.index import PackageFinder
|
||||
from pipenv.patched.notpip._internal.index.package_finder import PackageFinder
|
||||
from pipenv.patched.notpip._internal.operations.prepare import RequirementPreparer
|
||||
from pipenv.patched.notpip._internal.req.req_install import InstallRequirement
|
||||
from pipenv.patched.notpip._internal.req.req_set import RequirementSet
|
||||
@@ -54,6 +49,7 @@ if MYPY_CHECK_RUNNING:
|
||||
InstallRequirementProvider = Callable[
|
||||
[str, InstallRequirement], InstallRequirement
|
||||
]
|
||||
DiscoveredDependencies = DefaultDict[str, List[InstallRequirement]]
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -116,7 +112,6 @@ class Resolver(object):
|
||||
def __init__(
|
||||
self,
|
||||
preparer, # type: RequirementPreparer
|
||||
session, # type: PipSession
|
||||
finder, # type: PackageFinder
|
||||
make_install_req, # type: InstallRequirementProvider
|
||||
use_user_site, # type: bool
|
||||
@@ -141,10 +136,6 @@ class Resolver(object):
|
||||
|
||||
self.preparer = preparer
|
||||
self.finder = finder
|
||||
self.session = session
|
||||
|
||||
# This is set in resolve
|
||||
self.require_hashes = None # type: Optional[bool]
|
||||
|
||||
self.upgrade_strategy = upgrade_strategy
|
||||
self.force_reinstall = force_reinstall
|
||||
@@ -159,7 +150,7 @@ class Resolver(object):
|
||||
self.ignore_requires_python = True
|
||||
|
||||
self._discovered_dependencies = \
|
||||
defaultdict(list) # type: DefaultDict[str, List]
|
||||
defaultdict(list) # type: DiscoveredDependencies
|
||||
|
||||
def resolve(self, requirement_set):
|
||||
# type: (RequirementSet) -> None
|
||||
@@ -173,26 +164,12 @@ class Resolver(object):
|
||||
possible to move the preparation to become a step separated from
|
||||
dependency resolution.
|
||||
"""
|
||||
# make the wheelhouse
|
||||
if self.preparer.wheel_download_dir:
|
||||
ensure_dir(self.preparer.wheel_download_dir)
|
||||
|
||||
# If any top-level requirement has a hash specified, enter
|
||||
# hash-checking mode, which requires hashes from all.
|
||||
root_reqs = (
|
||||
requirement_set.unnamed_requirements +
|
||||
list(requirement_set.requirements.values())
|
||||
)
|
||||
self.require_hashes = (
|
||||
requirement_set.require_hashes or
|
||||
any(req.has_hash_options for req in root_reqs)
|
||||
)
|
||||
|
||||
# Display where finder is looking for packages
|
||||
search_scope = self.finder.search_scope
|
||||
locations = search_scope.get_formatted_locations()
|
||||
if locations:
|
||||
logger.info(locations)
|
||||
|
||||
# Actually prepare the files, and collect any exceptions. Most hash
|
||||
# exceptions cannot be checked ahead of time, because
|
||||
@@ -202,9 +179,7 @@ class Resolver(object):
|
||||
hash_errors = HashErrors()
|
||||
for req in chain(root_reqs, discovered_reqs):
|
||||
try:
|
||||
discovered_reqs.extend(
|
||||
self._resolve_one(requirement_set, req)
|
||||
)
|
||||
discovered_reqs.extend(self._resolve_one(requirement_set, req))
|
||||
except HashError as exc:
|
||||
exc.req = req
|
||||
hash_errors.append(exc)
|
||||
@@ -230,7 +205,7 @@ class Resolver(object):
|
||||
# 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):
|
||||
req.conflicts_with = req.satisfied_by
|
||||
req.should_reinstall = True
|
||||
req.satisfied_by = None
|
||||
|
||||
def _check_skip_installed(self, req_to_install):
|
||||
@@ -291,14 +266,8 @@ class Resolver(object):
|
||||
"""Takes a InstallRequirement and returns a single AbstractDist \
|
||||
representing a prepared variant of the same.
|
||||
"""
|
||||
assert self.require_hashes is not None, (
|
||||
"require_hashes should have been set in Resolver.resolve()"
|
||||
)
|
||||
|
||||
if req.editable:
|
||||
return self.preparer.prepare_editable_requirement(
|
||||
req, self.require_hashes, self.use_user_site, self.finder,
|
||||
)
|
||||
return self.preparer.prepare_editable_requirement(req)
|
||||
|
||||
# satisfied_by is only evaluated by calling _check_skip_installed,
|
||||
# so it must be None here.
|
||||
@@ -307,16 +276,15 @@ class Resolver(object):
|
||||
|
||||
if req.satisfied_by:
|
||||
return self.preparer.prepare_installed_requirement(
|
||||
req, self.require_hashes, skip_reason
|
||||
req, skip_reason
|
||||
)
|
||||
|
||||
upgrade_allowed = self._is_upgrade_allowed(req)
|
||||
|
||||
# We eagerly populate the link, since that's our "legacy" behavior.
|
||||
req.populate_link(self.finder, upgrade_allowed, self.require_hashes)
|
||||
abstract_dist = self.preparer.prepare_linked_requirement(
|
||||
req, self.session, self.finder, self.require_hashes
|
||||
)
|
||||
require_hashes = self.preparer.require_hashes
|
||||
req.populate_link(self.finder, upgrade_allowed, require_hashes)
|
||||
abstract_dist = self.preparer.prepare_linked_requirement(req)
|
||||
|
||||
# NOTE
|
||||
# The following portion is for determining if a certain package is
|
||||
@@ -413,10 +381,13 @@ class Resolver(object):
|
||||
# can refer to it when adding dependencies.
|
||||
if not requirement_set.has_requirement(req_to_install.name):
|
||||
# 'unnamed' requirements will get added here
|
||||
# 'unnamed' requirements can only come from being directly
|
||||
# provided by the user.
|
||||
req_to_install.is_direct = True
|
||||
assert req_to_install.is_direct
|
||||
available_requested = sorted(
|
||||
set(dist.extras) & set(req_to_install.extras)
|
||||
)
|
||||
req_to_install.is_direct = True
|
||||
requirement_set.add_requirement(
|
||||
req_to_install, parent_req_name=None,
|
||||
extras_requested=available_requested,
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
# The following comment should be removed at some point in the future.
|
||||
# mypy: strict-optional=False
|
||||
# mypy: disallow-untyped-defs=False
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
@@ -14,14 +13,18 @@ import sys
|
||||
import sysconfig
|
||||
from distutils import sysconfig as distutils_sysconfig
|
||||
from distutils.command.install import SCHEME_KEYS # type: ignore
|
||||
from distutils.command.install import install as distutils_install_command
|
||||
|
||||
from pipenv.patched.notpip._internal.models.scheme import Scheme
|
||||
from pipenv.patched.notpip._internal.utils import appdirs
|
||||
from pipenv.patched.notpip._internal.utils.compat import WINDOWS
|
||||
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
|
||||
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING, cast
|
||||
from pipenv.patched.notpip._internal.utils.virtualenv import running_under_virtualenv
|
||||
|
||||
if MYPY_CHECK_RUNNING:
|
||||
from typing import Any, Union, Dict, List, Optional
|
||||
from typing import Dict, List, Optional, Union
|
||||
|
||||
from distutils.cmd import Command as DistutilsCommand
|
||||
|
||||
|
||||
# Application Directories
|
||||
@@ -38,6 +41,7 @@ def get_major_minor_version():
|
||||
|
||||
|
||||
def get_src_prefix():
|
||||
# type: () -> str
|
||||
if running_under_virtualenv():
|
||||
src_prefix = os.path.join(sys.prefix, 'src')
|
||||
else:
|
||||
@@ -88,29 +92,25 @@ else:
|
||||
bin_py = '/usr/local/bin'
|
||||
|
||||
|
||||
def distutils_scheme(dist_name, user=False, home=None, root=None,
|
||||
isolated=False, prefix=None):
|
||||
# type:(str, bool, str, str, bool, str) -> dict
|
||||
def distutils_scheme(
|
||||
dist_name, user=False, home=None, root=None, isolated=False, prefix=None
|
||||
):
|
||||
# type:(str, bool, str, str, bool, str) -> Dict[str, str]
|
||||
"""
|
||||
Return a distutils install scheme
|
||||
"""
|
||||
from distutils.dist import Distribution
|
||||
|
||||
scheme = {}
|
||||
|
||||
if isolated:
|
||||
extra_dist_args = {"script_args": ["--no-user-cfg"]}
|
||||
else:
|
||||
extra_dist_args = {}
|
||||
dist_args = {'name': dist_name} # type: Dict[str, Union[str, List[str]]]
|
||||
dist_args.update(extra_dist_args)
|
||||
if isolated:
|
||||
dist_args["script_args"] = ["--no-user-cfg"]
|
||||
|
||||
d = Distribution(dist_args)
|
||||
# Ignoring, typeshed issue reported python/typeshed/issues/2567
|
||||
d.parse_config_files()
|
||||
# NOTE: Ignoring type since mypy can't find attributes on 'Command'
|
||||
i = d.get_command_obj('install', create=True) # type: Any
|
||||
assert i is not None
|
||||
obj = None # type: Optional[DistutilsCommand]
|
||||
obj = d.get_command_obj('install', create=True)
|
||||
assert obj is not None
|
||||
i = cast(distutils_install_command, obj)
|
||||
# NOTE: setting user or home has the side-effect of creating the home dir
|
||||
# or user base for installations during finalize_options()
|
||||
# ideally, we'd prefer a scheme class that has no side-effects.
|
||||
@@ -123,6 +123,8 @@ def distutils_scheme(dist_name, user=False, home=None, root=None,
|
||||
i.home = home or i.home
|
||||
i.root = root or i.root
|
||||
i.finalize_options()
|
||||
|
||||
scheme = {}
|
||||
for key in SCHEME_KEYS:
|
||||
scheme[key] = getattr(i, 'install_' + key)
|
||||
|
||||
@@ -131,9 +133,7 @@ def distutils_scheme(dist_name, user=False, home=None, root=None,
|
||||
# platlib). Note, i.install_lib is *always* set after
|
||||
# finalize_options(); we only want to override here if the user
|
||||
# has explicitly requested it hence going back to the config
|
||||
|
||||
# Ignoring, typeshed issue reported python/typeshed/issues/2567
|
||||
if 'install_lib' in d.get_option_dict('install'): # type: ignore
|
||||
if 'install_lib' in d.get_option_dict('install'):
|
||||
scheme.update(dict(purelib=i.install_lib, platlib=i.install_lib))
|
||||
|
||||
if running_under_virtualenv():
|
||||
@@ -154,3 +154,41 @@ def distutils_scheme(dist_name, user=False, home=None, root=None,
|
||||
)
|
||||
|
||||
return scheme
|
||||
|
||||
|
||||
def get_scheme(
|
||||
dist_name, # type: str
|
||||
user=False, # type: bool
|
||||
home=None, # type: Optional[str]
|
||||
root=None, # type: Optional[str]
|
||||
isolated=False, # type: bool
|
||||
prefix=None, # type: Optional[str]
|
||||
):
|
||||
# type: (...) -> Scheme
|
||||
"""
|
||||
Get the "scheme" corresponding to the input parameters. The distutils
|
||||
documentation provides the context for the available schemes:
|
||||
https://docs.python.org/3/install/index.html#alternate-installation
|
||||
|
||||
:param dist_name: the name of the package to retrieve the scheme for, used
|
||||
in the headers scheme path
|
||||
:param user: indicates to use the "user" scheme
|
||||
:param home: indicates to use the "home" scheme and provides the base
|
||||
directory for the same
|
||||
:param root: root under which other directories are re-based
|
||||
:param isolated: equivalent to --no-user-cfg, i.e. do not consider
|
||||
~/.pydistutils.cfg (posix) or ~/pydistutils.cfg (non-posix) for
|
||||
scheme paths
|
||||
:param prefix: indicates to use the "prefix" scheme and provides the
|
||||
base directory for the same
|
||||
"""
|
||||
scheme = distutils_scheme(
|
||||
dist_name, user, home, root, isolated, prefix
|
||||
)
|
||||
return Scheme(
|
||||
platlib=scheme["platlib"],
|
||||
purelib=scheme["purelib"],
|
||||
headers=scheme["headers"],
|
||||
scripts=scheme["scripts"],
|
||||
data=scheme["data"],
|
||||
)
|
||||
|
||||
@@ -1,47 +1,16 @@
|
||||
"""Primary application entrypoint.
|
||||
"""
|
||||
# The following comment should be removed at some point in the future.
|
||||
# mypy: disallow-untyped-defs=False
|
||||
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import locale
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
from pipenv.patched.notpip._internal.cli.autocompletion import autocomplete
|
||||
from pipenv.patched.notpip._internal.cli.main_parser import parse_command
|
||||
from pipenv.patched.notpip._internal.commands import create_command
|
||||
from pipenv.patched.notpip._internal.exceptions import PipError
|
||||
from pipenv.patched.notpip._internal.utils import deprecation
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
if MYPY_CHECK_RUNNING:
|
||||
from typing import Optional, List
|
||||
|
||||
|
||||
def main(args=None):
|
||||
if args is None:
|
||||
args = sys.argv[1:]
|
||||
# type: (Optional[List[str]]) -> int
|
||||
"""This is preserved for old console scripts that may still be referencing
|
||||
it.
|
||||
|
||||
# Configure our deprecation warnings to be sent through loggers
|
||||
deprecation.install_warning_logger()
|
||||
For additional details, see https://github.com/pypa/pip/issues/7498.
|
||||
"""
|
||||
from pipenv.patched.notpip._internal.utils.entrypoints import _wrapper
|
||||
|
||||
autocomplete()
|
||||
|
||||
try:
|
||||
cmd_name, cmd_args = parse_command(args)
|
||||
except PipError as exc:
|
||||
sys.stderr.write("ERROR: %s" % exc)
|
||||
sys.stderr.write(os.linesep)
|
||||
sys.exit(1)
|
||||
|
||||
# Needed for locale.getpreferredencoding(False) to work
|
||||
# in pip._internal.utils.encoding.auto_decode
|
||||
try:
|
||||
locale.setlocale(locale.LC_ALL, '')
|
||||
except locale.Error as e:
|
||||
# setlocale can apparently crash if locale are uninitialized
|
||||
logger.debug("Ignoring error %s when setting locale", e)
|
||||
command = create_command(cmd_name, isolated=("--isolated" in cmd_args))
|
||||
|
||||
return command.main(cmd_args)
|
||||
return _wrapper(args)
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
# The following comment should be removed at some point in the future.
|
||||
# mypy: disallow-untyped-defs=False
|
||||
|
||||
from pipenv.patched.notpip._vendor.packaging.version import parse as parse_version
|
||||
|
||||
from pipenv.patched.notpip._internal.utils.models import KeyBasedCompareMixin
|
||||
@@ -9,32 +6,32 @@ from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
|
||||
if MYPY_CHECK_RUNNING:
|
||||
from pipenv.patched.notpip._vendor.packaging.version import _BaseVersion
|
||||
from pipenv.patched.notpip._internal.models.link import Link
|
||||
from typing import Any
|
||||
|
||||
|
||||
class InstallationCandidate(KeyBasedCompareMixin):
|
||||
"""Represents a potential "candidate" for installation.
|
||||
"""
|
||||
|
||||
def __init__(self, project, version, link, requires_python=None):
|
||||
# type: (Any, str, Link, Any) -> None
|
||||
self.project = project
|
||||
def __init__(self, name, version, link, requires_python=None):
|
||||
# type: (str, str, Link, Any) -> None
|
||||
self.name = name
|
||||
self.version = parse_version(version) # type: _BaseVersion
|
||||
self.link = link
|
||||
self.requires_python = requires_python
|
||||
|
||||
super(InstallationCandidate, self).__init__(
|
||||
key=(self.project, self.version, self.link),
|
||||
key=(self.name, self.version, self.link),
|
||||
defining_class=InstallationCandidate
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
# type: () -> str
|
||||
return "<InstallationCandidate({!r}, {!r}, {!r})>".format(
|
||||
self.project, self.version, self.link,
|
||||
self.name, self.version, self.link,
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
# type: () -> str
|
||||
return '{!r} candidate (version {} at {})'.format(
|
||||
self.project, self.version, self.link,
|
||||
self.name, self.version, self.link,
|
||||
)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
# The following comment should be removed at some point in the future.
|
||||
# mypy: strict-optional=False
|
||||
# mypy: disallow-untyped-defs=False
|
||||
|
||||
from pipenv.patched.notpip._vendor.packaging.utils import canonicalize_name
|
||||
|
||||
@@ -16,7 +15,7 @@ class FormatControl(object):
|
||||
"""
|
||||
|
||||
def __init__(self, no_binary=None, only_binary=None):
|
||||
# type: (Optional[Set], Optional[Set]) -> None
|
||||
# type: (Optional[Set[str]], Optional[Set[str]]) -> None
|
||||
if no_binary is None:
|
||||
no_binary = set()
|
||||
if only_binary is None:
|
||||
@@ -26,12 +25,15 @@ class FormatControl(object):
|
||||
self.only_binary = only_binary
|
||||
|
||||
def __eq__(self, other):
|
||||
# type: (object) -> bool
|
||||
return self.__dict__ == other.__dict__
|
||||
|
||||
def __ne__(self, other):
|
||||
# type: (object) -> bool
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __repr__(self):
|
||||
# type: () -> str
|
||||
return "{}({}, {})".format(
|
||||
self.__class__.__name__,
|
||||
self.no_binary,
|
||||
@@ -40,7 +42,7 @@ class FormatControl(object):
|
||||
|
||||
@staticmethod
|
||||
def handle_mutual_excludes(value, target, other):
|
||||
# type: (str, Optional[Set], Optional[Set]) -> None
|
||||
# type: (str, Optional[Set[str]], Optional[Set[str]]) -> None
|
||||
if value.startswith('-'):
|
||||
raise CommandError(
|
||||
"--no-binary / --only-binary option requires 1 argument."
|
||||
@@ -63,7 +65,7 @@ class FormatControl(object):
|
||||
target.add(name)
|
||||
|
||||
def get_allowed_formats(self, canonical_name):
|
||||
# type: (str) -> FrozenSet
|
||||
# type: (str) -> FrozenSet[str]
|
||||
result = {"binary", "source"}
|
||||
if canonical_name in self.only_binary:
|
||||
result.discard('source')
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
# The following comment should be removed at some point in the future.
|
||||
# mypy: disallow-untyped-defs=False
|
||||
|
||||
import os
|
||||
import posixpath
|
||||
import re
|
||||
@@ -19,7 +16,7 @@ from pipenv.patched.notpip._internal.utils.urls import path_to_url, url_to_path
|
||||
|
||||
if MYPY_CHECK_RUNNING:
|
||||
from typing import Optional, Text, Tuple, Union
|
||||
from pipenv.patched.notpip._internal.collector import HTMLPage
|
||||
from pipenv.patched.notpip._internal.index.collector import HTMLPage
|
||||
from pipenv.patched.notpip._internal.utils.hashes import Hashes
|
||||
|
||||
|
||||
@@ -67,6 +64,7 @@ class Link(KeyBasedCompareMixin):
|
||||
super(Link, self).__init__(key=url, defining_class=Link)
|
||||
|
||||
def __str__(self):
|
||||
# type: () -> str
|
||||
if self.requires_python:
|
||||
rp = ' (requires-python:%s)' % self.requires_python
|
||||
else:
|
||||
@@ -78,6 +76,7 @@ class Link(KeyBasedCompareMixin):
|
||||
return redact_auth_from_url(str(self._url))
|
||||
|
||||
def __repr__(self):
|
||||
# type: () -> str
|
||||
return '<Link %s>' % self
|
||||
|
||||
@property
|
||||
@@ -180,7 +179,7 @@ class Link(KeyBasedCompareMixin):
|
||||
|
||||
@property
|
||||
def show_url(self):
|
||||
# type: () -> Optional[str]
|
||||
# type: () -> str
|
||||
return posixpath.basename(self._url.split('#', 1)[0].split('?', 1)[0])
|
||||
|
||||
@property
|
||||
@@ -211,6 +210,7 @@ class Link(KeyBasedCompareMixin):
|
||||
|
||||
@property
|
||||
def has_hash(self):
|
||||
# type: () -> bool
|
||||
return self.hash_name is not None
|
||||
|
||||
def is_hash_allowed(self, hashes):
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
"""
|
||||
For types associated with installation schemes.
|
||||
|
||||
For a general overview of available schemes and their context, see
|
||||
https://docs.python.org/3/install/index.html#alternate-installation.
|
||||
"""
|
||||
|
||||
|
||||
class Scheme(object):
|
||||
"""A Scheme holds paths which are used as the base directories for
|
||||
artifacts associated with a Python package.
|
||||
"""
|
||||
def __init__(
|
||||
self,
|
||||
platlib, # type: str
|
||||
purelib, # type: str
|
||||
headers, # type: str
|
||||
scripts, # type: str
|
||||
data, # type: str
|
||||
):
|
||||
self.platlib = platlib
|
||||
self.purelib = purelib
|
||||
self.headers = headers
|
||||
self.scripts = scripts
|
||||
self.data = data
|
||||
@@ -1,6 +1,3 @@
|
||||
# The following comment should be removed at some point in the future.
|
||||
# mypy: disallow-untyped-defs=False
|
||||
|
||||
import itertools
|
||||
import logging
|
||||
import os
|
||||
@@ -10,7 +7,7 @@ from pipenv.patched.notpip._vendor.packaging.utils import canonicalize_name
|
||||
from pipenv.patched.notpip._vendor.six.moves.urllib import parse as urllib_parse
|
||||
|
||||
from pipenv.patched.notpip._internal.models.index import PyPI
|
||||
from pipenv.patched.notpip._internal.utils.compat import HAS_TLS
|
||||
from pipenv.patched.notpip._internal.utils.compat import has_tls
|
||||
from pipenv.patched.notpip._internal.utils.misc import normalize_path, redact_auth_from_url
|
||||
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
|
||||
|
||||
@@ -52,7 +49,7 @@ class SearchScope(object):
|
||||
|
||||
# If we don't have TLS enabled, then WARN if anyplace we're looking
|
||||
# relies on TLS.
|
||||
if not HAS_TLS:
|
||||
if not has_tls():
|
||||
for link in itertools.chain(index_urls, built_find_links):
|
||||
parsed = urllib_parse.urlparse(link)
|
||||
if parsed.scheme == 'https':
|
||||
@@ -101,6 +98,7 @@ class SearchScope(object):
|
||||
"""
|
||||
|
||||
def mkurl_pypi_url(url):
|
||||
# type: (str) -> str
|
||||
loc = posixpath.join(
|
||||
url,
|
||||
urllib_parse.quote(canonicalize_name(project_name)))
|
||||
|
||||
@@ -6,7 +6,8 @@ from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
|
||||
|
||||
if MYPY_CHECK_RUNNING:
|
||||
from typing import List, Optional, Tuple
|
||||
from pipenv.patched.notpip._internal.pep425tags import Pep425Tag
|
||||
|
||||
from pipenv.patched.notpip._vendor.packaging.tags import Tag
|
||||
|
||||
|
||||
class TargetPython(object):
|
||||
@@ -55,7 +56,7 @@ class TargetPython(object):
|
||||
self.py_version_info = py_version_info
|
||||
|
||||
# This is used to cache the return value of get_tags().
|
||||
self._valid_tags = None # type: Optional[List[Pep425Tag]]
|
||||
self._valid_tags = None # type: Optional[List[Tag]]
|
||||
|
||||
def format_given(self):
|
||||
# type: () -> str
|
||||
@@ -80,7 +81,7 @@ class TargetPython(object):
|
||||
)
|
||||
|
||||
def get_tags(self):
|
||||
# type: () -> List[Pep425Tag]
|
||||
# type: () -> List[Tag]
|
||||
"""
|
||||
Return the supported PEP 425 tags to check wheel candidates against.
|
||||
|
||||
@@ -91,12 +92,12 @@ class TargetPython(object):
|
||||
# versions=None uses special default logic.
|
||||
py_version_info = self._given_py_version_info
|
||||
if py_version_info is None:
|
||||
versions = None
|
||||
version = None
|
||||
else:
|
||||
versions = [version_info_to_nodot(py_version_info)]
|
||||
version = version_info_to_nodot(py_version_info)
|
||||
|
||||
tags = get_supported(
|
||||
versions=versions,
|
||||
version=version,
|
||||
platform=self.platform,
|
||||
abi=self.abi,
|
||||
impl=self.implementation,
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
"""Represents a wheel file and provides access to the various parts of the
|
||||
name that have meaning.
|
||||
"""
|
||||
import re
|
||||
|
||||
from pipenv.patched.notpip._vendor.packaging.tags import Tag
|
||||
|
||||
from pipenv.patched.notpip._internal.exceptions import InvalidWheelFilename
|
||||
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
|
||||
|
||||
if MYPY_CHECK_RUNNING:
|
||||
from typing import List
|
||||
|
||||
|
||||
class Wheel(object):
|
||||
"""A wheel file"""
|
||||
|
||||
wheel_file_re = re.compile(
|
||||
r"""^(?P<namever>(?P<name>.+?)-(?P<ver>.*?))
|
||||
((-(?P<build>\d[^-]*?))?-(?P<pyver>.+?)-(?P<abi>.+?)-(?P<plat>.+?)
|
||||
\.whl|\.dist-info)$""",
|
||||
re.VERBOSE
|
||||
)
|
||||
|
||||
def __init__(self, filename):
|
||||
# type: (str) -> None
|
||||
"""
|
||||
:raises InvalidWheelFilename: when the filename is invalid for a wheel
|
||||
"""
|
||||
wheel_info = self.wheel_file_re.match(filename)
|
||||
if not wheel_info:
|
||||
raise InvalidWheelFilename(
|
||||
"%s is not a valid wheel filename." % filename
|
||||
)
|
||||
self.filename = filename
|
||||
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('.')
|
||||
|
||||
# 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
|
||||
}
|
||||
|
||||
def get_formatted_file_tags(self):
|
||||
# type: () -> List[str]
|
||||
"""Return the wheel's tags as a sorted list of strings."""
|
||||
return sorted(str(tag) for tag in self.file_tags)
|
||||
|
||||
def support_index_min(self, tags):
|
||||
# type: (List[Tag]) -> int
|
||||
"""Return the lowest index that one of the wheel's file_tag combinations
|
||||
achieves in the given list of supported tags.
|
||||
|
||||
For example, if there are 8 supported tags and one of the file tags
|
||||
is first in the list, then return 0.
|
||||
|
||||
:param tags: the PEP 425 tags to check the wheel against, in order
|
||||
with most preferred first.
|
||||
|
||||
:raises ValueError: If none of the wheel's file tags match one of
|
||||
the supported tags.
|
||||
"""
|
||||
return min(tags.index(tag) for tag in self.file_tags if tag in tags)
|
||||
|
||||
def supported(self, tags):
|
||||
# type: (List[Tag]) -> bool
|
||||
"""Return whether the wheel is compatible with one of the given tags.
|
||||
|
||||
:param tags: the PEP 425 tags to check the wheel against.
|
||||
"""
|
||||
return not self.file_tags.isdisjoint(tags)
|
||||
@@ -9,6 +9,7 @@ from contextlib import contextmanager
|
||||
|
||||
from pipenv.patched.notpip._vendor.cachecontrol.cache import BaseCache
|
||||
from pipenv.patched.notpip._vendor.cachecontrol.caches import FileCache
|
||||
from pipenv.patched.notpip._vendor.requests.models import Response
|
||||
|
||||
from pipenv.patched.notpip._internal.utils.filesystem import adjacent_tmp_file, replace
|
||||
from pipenv.patched.notpip._internal.utils.misc import ensure_dir
|
||||
@@ -18,6 +19,11 @@ if MYPY_CHECK_RUNNING:
|
||||
from typing import Optional
|
||||
|
||||
|
||||
def is_from_cache(response):
|
||||
# type: (Response) -> bool
|
||||
return getattr(response, "from_cache", False)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def suppressed_cache_errors():
|
||||
"""If we can't access the cache then we can just skip caching and process
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
"""Download files with progress indicators.
|
||||
"""
|
||||
import cgi
|
||||
import logging
|
||||
import mimetypes
|
||||
import os
|
||||
|
||||
from pipenv.patched.notpip._vendor import requests
|
||||
from pipenv.patched.notpip._vendor.requests.models import CONTENT_CHUNK_SIZE
|
||||
|
||||
from pipenv.patched.notpip._internal.models.index import PyPI
|
||||
from pipenv.patched.notpip._internal.network.cache import is_from_cache
|
||||
from pipenv.patched.notpip._internal.network.utils import response_chunks
|
||||
from pipenv.patched.notpip._internal.utils.misc import (
|
||||
format_size,
|
||||
redact_auth_from_url,
|
||||
splitext,
|
||||
)
|
||||
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
|
||||
from pipenv.patched.notpip._internal.utils.ui import DownloadProgressProvider
|
||||
|
||||
if MYPY_CHECK_RUNNING:
|
||||
from typing import Iterable, Optional
|
||||
|
||||
from pipenv.patched.notpip._vendor.requests.models import Response
|
||||
|
||||
from pipenv.patched.notpip._internal.models.link import Link
|
||||
from pipenv.patched.notpip._internal.network.session import PipSession
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _get_http_response_size(resp):
|
||||
# type: (Response) -> Optional[int]
|
||||
try:
|
||||
return int(resp.headers['content-length'])
|
||||
except (ValueError, KeyError, TypeError):
|
||||
return None
|
||||
|
||||
|
||||
def _prepare_download(
|
||||
resp, # type: Response
|
||||
link, # type: Link
|
||||
progress_bar # type: str
|
||||
):
|
||||
# type: (...) -> Iterable[bytes]
|
||||
total_length = _get_http_response_size(resp)
|
||||
|
||||
if link.netloc == PyPI.file_storage_domain:
|
||||
url = link.show_url
|
||||
else:
|
||||
url = link.url_without_fragment
|
||||
|
||||
logged_url = redact_auth_from_url(url)
|
||||
|
||||
if total_length:
|
||||
logged_url = '{} ({})'.format(logged_url, format_size(total_length))
|
||||
|
||||
if is_from_cache(resp):
|
||||
logger.info("Using cached %s", logged_url)
|
||||
else:
|
||||
logger.info("Downloading %s", logged_url)
|
||||
|
||||
if logger.getEffectiveLevel() > logging.INFO:
|
||||
show_progress = False
|
||||
elif is_from_cache(resp):
|
||||
show_progress = False
|
||||
elif not total_length:
|
||||
show_progress = True
|
||||
elif total_length > (40 * 1000):
|
||||
show_progress = True
|
||||
else:
|
||||
show_progress = False
|
||||
|
||||
chunks = response_chunks(resp, CONTENT_CHUNK_SIZE)
|
||||
|
||||
if not show_progress:
|
||||
return chunks
|
||||
|
||||
return DownloadProgressProvider(
|
||||
progress_bar, max=total_length
|
||||
)(chunks)
|
||||
|
||||
|
||||
def sanitize_content_filename(filename):
|
||||
# type: (str) -> str
|
||||
"""
|
||||
Sanitize the "filename" value from a Content-Disposition header.
|
||||
"""
|
||||
return os.path.basename(filename)
|
||||
|
||||
|
||||
def parse_content_disposition(content_disposition, default_filename):
|
||||
# type: (str, str) -> str
|
||||
"""
|
||||
Parse the "filename" value from a Content-Disposition header, and
|
||||
return the default filename if the result is empty.
|
||||
"""
|
||||
_type, params = cgi.parse_header(content_disposition)
|
||||
filename = params.get('filename')
|
||||
if filename:
|
||||
# We need to sanitize the filename to prevent directory traversal
|
||||
# in case the filename contains ".." path parts.
|
||||
filename = sanitize_content_filename(filename)
|
||||
return filename or default_filename
|
||||
|
||||
|
||||
def _get_http_response_filename(resp, link):
|
||||
# type: (Response, Link) -> str
|
||||
"""Get an ideal filename from the given HTTP response, falling back to
|
||||
the link filename if not provided.
|
||||
"""
|
||||
filename = link.filename # fallback
|
||||
# Have a look at the Content-Disposition header for a better guess
|
||||
content_disposition = resp.headers.get('content-disposition')
|
||||
if content_disposition:
|
||||
filename = parse_content_disposition(content_disposition, filename)
|
||||
ext = splitext(filename)[1] # type: Optional[str]
|
||||
if not ext:
|
||||
ext = mimetypes.guess_extension(
|
||||
resp.headers.get('content-type', '')
|
||||
)
|
||||
if ext:
|
||||
filename += ext
|
||||
if not ext and link.url != resp.url:
|
||||
ext = os.path.splitext(resp.url)[1]
|
||||
if ext:
|
||||
filename += ext
|
||||
return filename
|
||||
|
||||
|
||||
def _http_get_download(session, link):
|
||||
# type: (PipSession, Link) -> Response
|
||||
target_url = link.url.split('#', 1)[0]
|
||||
resp = session.get(
|
||||
target_url,
|
||||
# We use Accept-Encoding: identity here because requests
|
||||
# defaults to accepting compressed responses. This breaks in
|
||||
# a variety of ways depending on how the server is configured.
|
||||
# - Some servers will notice that the file isn't a compressible
|
||||
# file and will leave the file alone and with an empty
|
||||
# Content-Encoding
|
||||
# - Some servers will notice that the file is already
|
||||
# compressed and will leave the file alone and will add a
|
||||
# Content-Encoding: gzip header
|
||||
# - Some servers won't notice anything at all and will take
|
||||
# a file that's already been compressed and compress it again
|
||||
# and set the Content-Encoding: gzip header
|
||||
# By setting this to request only the identity encoding We're
|
||||
# hoping to eliminate the third case. Hopefully there does not
|
||||
# exist a server which when given a file will notice it is
|
||||
# already compressed and that you're not asking for a
|
||||
# compressed file and will then decompress it before sending
|
||||
# because if that's the case I don't think it'll ever be
|
||||
# possible to make this work.
|
||||
headers={"Accept-Encoding": "identity"},
|
||||
stream=True,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
return resp
|
||||
|
||||
|
||||
class Download(object):
|
||||
def __init__(
|
||||
self,
|
||||
response, # type: Response
|
||||
filename, # type: str
|
||||
chunks, # type: Iterable[bytes]
|
||||
):
|
||||
# type: (...) -> None
|
||||
self.response = response
|
||||
self.filename = filename
|
||||
self.chunks = chunks
|
||||
|
||||
|
||||
class Downloader(object):
|
||||
def __init__(
|
||||
self,
|
||||
session, # type: PipSession
|
||||
progress_bar, # type: str
|
||||
):
|
||||
# type: (...) -> None
|
||||
self._session = session
|
||||
self._progress_bar = progress_bar
|
||||
|
||||
def __call__(self, link):
|
||||
# type: (Link) -> Download
|
||||
try:
|
||||
resp = _http_get_download(self._session, link)
|
||||
except requests.HTTPError as e:
|
||||
logger.critical(
|
||||
"HTTP error %s while getting %s", e.response.status_code, link
|
||||
)
|
||||
raise
|
||||
|
||||
return Download(
|
||||
resp,
|
||||
_get_http_response_filename(resp, link),
|
||||
_prepare_download(resp, link, self._progress_bar),
|
||||
)
|
||||
@@ -26,8 +26,7 @@ from pipenv.patched.notpip import __version__
|
||||
from pipenv.patched.notpip._internal.network.auth import MultiDomainBasicAuth
|
||||
from pipenv.patched.notpip._internal.network.cache import SafeFileCache
|
||||
# Import ssl from compat so the initial import occurs in only one place.
|
||||
from pipenv.patched.notpip._internal.utils.compat import HAS_TLS, ipaddress, ssl
|
||||
from pipenv.patched.notpip._internal.utils.filesystem import check_path_owner
|
||||
from pipenv.patched.notpip._internal.utils.compat import has_tls, ipaddress
|
||||
from pipenv.patched.notpip._internal.utils.glibc import libc_ver
|
||||
from pipenv.patched.notpip._internal.utils.misc import (
|
||||
build_url_from_netloc,
|
||||
@@ -153,7 +152,8 @@ def user_agent():
|
||||
if platform.machine():
|
||||
data["cpu"] = platform.machine()
|
||||
|
||||
if HAS_TLS:
|
||||
if has_tls():
|
||||
import _ssl as ssl
|
||||
data["openssl_version"] = ssl.OPENSSL_VERSION
|
||||
|
||||
setuptools_version = get_installed_version("setuptools")
|
||||
@@ -212,8 +212,9 @@ class LocalFSAdapter(BaseAdapter):
|
||||
class InsecureHTTPAdapter(HTTPAdapter):
|
||||
|
||||
def cert_verify(self, conn, url, verify, cert):
|
||||
conn.cert_reqs = 'CERT_NONE'
|
||||
conn.ca_certs = None
|
||||
super(InsecureHTTPAdapter, self).cert_verify(
|
||||
conn=conn, url=url, verify=False, cert=cert
|
||||
)
|
||||
|
||||
|
||||
class PipSession(requests.Session):
|
||||
@@ -262,19 +263,6 @@ class PipSession(requests.Session):
|
||||
backoff_factor=0.25,
|
||||
)
|
||||
|
||||
# Check to ensure that the directory containing our cache directory
|
||||
# is owned by the user current executing pip. If it does not exist
|
||||
# we will check the parent directory until we find one that does exist.
|
||||
if cache and not check_path_owner(cache):
|
||||
logger.warning(
|
||||
"The directory '%s' or its parent directory is not owned by "
|
||||
"the current user and the cache has been disabled. Please "
|
||||
"check the permissions and owner of that directory. If "
|
||||
"executing pip with sudo, you may want sudo's -H flag.",
|
||||
cache,
|
||||
)
|
||||
cache = None
|
||||
|
||||
# We want to _only_ cache responses on securely fetched origins. We do
|
||||
# this because we can't validate the response of an insecurely fetched
|
||||
# origin, and we don't want someone to be able to poison the cache and
|
||||
@@ -360,22 +348,13 @@ class PipSession(requests.Session):
|
||||
continue
|
||||
|
||||
try:
|
||||
# We need to do this decode dance to ensure that we have a
|
||||
# unicode object, even on Python 2.x.
|
||||
addr = ipaddress.ip_address(
|
||||
origin_host
|
||||
if (
|
||||
isinstance(origin_host, six.text_type) or
|
||||
origin_host is None
|
||||
)
|
||||
else origin_host.decode("utf8")
|
||||
None
|
||||
if origin_host is None
|
||||
else six.ensure_text(origin_host)
|
||||
)
|
||||
network = ipaddress.ip_network(
|
||||
secure_host
|
||||
if isinstance(secure_host, six.text_type)
|
||||
# setting secure_host to proper Union[bytes, str]
|
||||
# creates problems in other places
|
||||
else secure_host.decode("utf8") # type: ignore
|
||||
six.ensure_text(secure_host)
|
||||
)
|
||||
except ValueError:
|
||||
# We don't have both a valid address or a valid network, so
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
from pipenv.patched.notpip._vendor.requests.models import CONTENT_CHUNK_SIZE, Response
|
||||
|
||||
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
|
||||
|
||||
if MYPY_CHECK_RUNNING:
|
||||
from typing import Iterator
|
||||
|
||||
|
||||
def response_chunks(response, chunk_size=CONTENT_CHUNK_SIZE):
|
||||
# type: (Response, int) -> Iterator[bytes]
|
||||
"""Given a requests Response, provide the data chunks.
|
||||
"""
|
||||
try:
|
||||
# Special case for urllib3.
|
||||
for chunk in response.raw.stream(
|
||||
chunk_size,
|
||||
# We use decode_content=False here because we don't
|
||||
# want urllib3 to mess with the raw bytes we get
|
||||
# from the server. If we decompress inside of
|
||||
# urllib3 then we cannot verify the checksum
|
||||
# because the checksum will be of the compressed
|
||||
# file. This breakage will only occur if the
|
||||
# server adds a Content-Encoding header, which
|
||||
# depends on how the server was configured:
|
||||
# - Some servers will notice that the file isn't a
|
||||
# compressible file and will leave the file alone
|
||||
# and with an empty Content-Encoding
|
||||
# - Some servers will notice that the file is
|
||||
# already compressed and will leave the file
|
||||
# alone and will add a Content-Encoding: gzip
|
||||
# header
|
||||
# - Some servers won't notice anything at all and
|
||||
# will take a file that's already been compressed
|
||||
# and compress it again and set the
|
||||
# Content-Encoding: gzip header
|
||||
#
|
||||
# By setting this not to decode automatically we
|
||||
# hope to eliminate problems with the second case.
|
||||
decode_content=False,
|
||||
):
|
||||
yield chunk
|
||||
except AttributeError:
|
||||
# Standard file-like object.
|
||||
while True:
|
||||
chunk = response.raw.read(chunk_size)
|
||||
if not chunk:
|
||||
break
|
||||
yield chunk
|
||||
@@ -0,0 +1,40 @@
|
||||
"""Metadata generation logic for source distributions.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
from pipenv.patched.notpip._internal.utils.subprocess import runner_with_spinner_message
|
||||
from pipenv.patched.notpip._internal.utils.temp_dir import TempDirectory
|
||||
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
|
||||
|
||||
if MYPY_CHECK_RUNNING:
|
||||
from pipenv.patched.notpip._internal.build_env import BuildEnvironment
|
||||
from pipenv.patched.notpip._vendor.pep517.wrappers import Pep517HookCaller
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def generate_metadata(build_env, backend):
|
||||
# type: (BuildEnvironment, Pep517HookCaller) -> str
|
||||
"""Generate metadata using mechanisms described in PEP 517.
|
||||
|
||||
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, so we don't have to
|
||||
# consider the possibility that this hook doesn't exist.
|
||||
runner = runner_with_spinner_message("Preparing wheel metadata")
|
||||
with backend.subprocess_runner(runner):
|
||||
distinfo_dir = backend.prepare_metadata_for_build_wheel(
|
||||
metadata_dir
|
||||
)
|
||||
|
||||
return os.path.join(metadata_dir, distinfo_dir)
|
||||
+33
-47
@@ -1,4 +1,4 @@
|
||||
"""Metadata generation logic for source distributions.
|
||||
"""Metadata generation logic for legacy source distributions.
|
||||
"""
|
||||
|
||||
import logging
|
||||
@@ -6,32 +6,19 @@ import os
|
||||
|
||||
from pipenv.patched.notpip._internal.exceptions import InstallationError
|
||||
from pipenv.patched.notpip._internal.utils.misc import ensure_dir
|
||||
from pipenv.patched.notpip._internal.utils.setuptools_build import make_setuptools_shim_args
|
||||
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.typing import MYPY_CHECK_RUNNING
|
||||
from pipenv.patched.notpip._internal.vcs import vcs
|
||||
|
||||
if MYPY_CHECK_RUNNING:
|
||||
from typing import Callable, List
|
||||
from pipenv.patched.notpip._internal.req.req_install import InstallRequirement
|
||||
from typing import List, Optional
|
||||
|
||||
from pipenv.patched.notpip._internal.build_env import BuildEnvironment
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_metadata_generator(install_req):
|
||||
# type: (InstallRequirement) -> Callable[[InstallRequirement], str]
|
||||
"""Return a callable metadata generator for this InstallRequirement.
|
||||
|
||||
A metadata generator takes an InstallRequirement (install_req) as an input,
|
||||
generates metadata via the appropriate process for that install_req and
|
||||
returns the generated metadata directory.
|
||||
"""
|
||||
if not install_req.use_pep517:
|
||||
return _generate_metadata_legacy
|
||||
|
||||
return _generate_metadata
|
||||
|
||||
|
||||
def _find_egg_info(source_directory, is_editable):
|
||||
# type: (str, bool) -> str
|
||||
"""Find an .egg-info in `source_directory`, based on `is_editable`.
|
||||
@@ -79,7 +66,7 @@ def _find_egg_info(source_directory, is_editable):
|
||||
|
||||
if not filenames:
|
||||
raise InstallationError(
|
||||
"Files/directories not found in %s" % base
|
||||
"Files/directories not found in {}".format(base)
|
||||
)
|
||||
|
||||
# If we have more than one match, we pick the toplevel one. This
|
||||
@@ -91,46 +78,45 @@ def _find_egg_info(source_directory, is_editable):
|
||||
return os.path.join(base, filenames[0])
|
||||
|
||||
|
||||
def _generate_metadata_legacy(install_req):
|
||||
# type: (InstallRequirement) -> str
|
||||
req_details_str = install_req.name or "from {}".format(install_req.link)
|
||||
def generate_metadata(
|
||||
build_env, # type: BuildEnvironment
|
||||
setup_py_path, # type: str
|
||||
source_dir, # type: str
|
||||
editable, # type: bool
|
||||
isolated, # type: bool
|
||||
details, # type: str
|
||||
):
|
||||
# type: (...) -> 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',
|
||||
install_req.setup_py_path, req_details_str,
|
||||
setup_py_path, details,
|
||||
)
|
||||
|
||||
# Compose arguments for subprocess call
|
||||
base_cmd = make_setuptools_shim_args(install_req.setup_py_path)
|
||||
if install_req.isolated:
|
||||
base_cmd += ["--no-user-cfg"]
|
||||
|
||||
egg_info_dir = None # type: Optional[str]
|
||||
# For non-editable installs, don't put the .egg-info files at the root,
|
||||
# to avoid confusion due to the source code being considered an installed
|
||||
# egg.
|
||||
egg_base_option = [] # type: List[str]
|
||||
if not install_req.editable:
|
||||
egg_info_dir = os.path.join(
|
||||
install_req.unpacked_source_directory, 'pip-egg-info',
|
||||
)
|
||||
egg_base_option = ['--egg-base', egg_info_dir]
|
||||
|
||||
if not editable:
|
||||
egg_info_dir = os.path.join(source_dir, 'pip-egg-info')
|
||||
# setuptools complains if the target directory does not exist.
|
||||
ensure_dir(egg_info_dir)
|
||||
|
||||
with install_req.build_env:
|
||||
args = make_setuptools_egg_info_args(
|
||||
setup_py_path,
|
||||
egg_info_dir=egg_info_dir,
|
||||
no_user_config=isolated,
|
||||
)
|
||||
|
||||
with build_env:
|
||||
call_subprocess(
|
||||
base_cmd + ["egg_info"] + egg_base_option,
|
||||
cwd=install_req.unpacked_source_directory,
|
||||
args,
|
||||
cwd=source_dir,
|
||||
command_desc='python setup.py egg_info',
|
||||
)
|
||||
|
||||
# Return the .egg-info directory.
|
||||
return _find_egg_info(
|
||||
install_req.unpacked_source_directory,
|
||||
install_req.editable,
|
||||
)
|
||||
|
||||
|
||||
def _generate_metadata(install_req):
|
||||
# type: (InstallRequirement) -> str
|
||||
return install_req.prepare_pep517_metadata()
|
||||
return _find_egg_info(source_dir, editable)
|
||||
@@ -0,0 +1,46 @@
|
||||
import logging
|
||||
import os
|
||||
|
||||
from pipenv.patched.notpip._internal.utils.subprocess import runner_with_spinner_message
|
||||
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
|
||||
|
||||
if MYPY_CHECK_RUNNING:
|
||||
from typing import List, Optional
|
||||
from pipenv.patched.notpip._vendor.pep517.wrappers import Pep517HookCaller
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def build_wheel_pep517(
|
||||
name, # type: str
|
||||
backend, # type: Pep517HookCaller
|
||||
metadata_directory, # type: str
|
||||
build_options, # type: List[str]
|
||||
tempd, # type: str
|
||||
):
|
||||
# type: (...) -> 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
|
||||
if build_options:
|
||||
# PEP 517 does not support --build-options
|
||||
logger.error('Cannot build wheel for %s using PEP 517 when '
|
||||
'--build-option is present' % (name,))
|
||||
return None
|
||||
try:
|
||||
logger.debug('Destination directory: %s', tempd)
|
||||
|
||||
runner = runner_with_spinner_message(
|
||||
'Building wheel for {} (PEP 517)'.format(name)
|
||||
)
|
||||
with backend.subprocess_runner(runner):
|
||||
wheel_name = backend.build_wheel(
|
||||
tempd,
|
||||
metadata_directory=metadata_directory,
|
||||
)
|
||||
except Exception:
|
||||
logger.error('Failed building wheel for %s', name)
|
||||
return None
|
||||
return os.path.join(tempd, wheel_name)
|
||||
@@ -0,0 +1,115 @@
|
||||
import logging
|
||||
import os.path
|
||||
|
||||
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.typing import MYPY_CHECK_RUNNING
|
||||
from pipenv.patched.notpip._internal.utils.ui import open_spinner
|
||||
|
||||
if MYPY_CHECK_RUNNING:
|
||||
from typing import List, Optional, Text
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def format_command_result(
|
||||
command_args, # type: List[str]
|
||||
command_output, # type: Text
|
||||
):
|
||||
# type: (...) -> str
|
||||
"""Format command information for logging."""
|
||||
command_desc = format_command_args(command_args)
|
||||
text = 'Command arguments: {}\n'.format(command_desc)
|
||||
|
||||
if not command_output:
|
||||
text += 'Command output: None'
|
||||
elif logger.getEffectiveLevel() > logging.DEBUG:
|
||||
text += 'Command output: [use --verbose to show]'
|
||||
else:
|
||||
if not command_output.endswith('\n'):
|
||||
command_output += '\n'
|
||||
text += 'Command output:\n{}{}'.format(command_output, LOG_DIVIDER)
|
||||
|
||||
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: Text
|
||||
):
|
||||
# type: (...) -> 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 += 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'
|
||||
).format(name, names)
|
||||
msg += format_command_result(command_args, command_output)
|
||||
logger.warning(msg)
|
||||
|
||||
return os.path.join(temp_dir, names[0])
|
||||
|
||||
|
||||
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]
|
||||
"""Build one unpacked package using the "legacy" build process.
|
||||
|
||||
Returns path to wheel if successfully built. Otherwise, returns None.
|
||||
"""
|
||||
wheel_args = make_setuptools_bdist_wheel_args(
|
||||
setup_py_path,
|
||||
global_options=global_options,
|
||||
build_options=build_options,
|
||||
destination_dir=tempd,
|
||||
)
|
||||
|
||||
spin_message = 'Building wheel for %s (setup.py)' % (name,)
|
||||
with open_spinner(spin_message) as spinner:
|
||||
logger.debug('Destination directory: %s', tempd)
|
||||
|
||||
try:
|
||||
output = call_subprocess(
|
||||
wheel_args,
|
||||
cwd=source_dir,
|
||||
spinner=spinner,
|
||||
)
|
||||
except Exception:
|
||||
spinner.finish("error")
|
||||
logger.error('Failed building wheel for %s', name)
|
||||
return None
|
||||
|
||||
names = os.listdir(tempd)
|
||||
wheel_path = get_legacy_build_wheel_path(
|
||||
names=names,
|
||||
temp_dir=tempd,
|
||||
name=name,
|
||||
command_args=wheel_args,
|
||||
command_output=output,
|
||||
)
|
||||
return wheel_path
|
||||
@@ -53,7 +53,7 @@ def create_package_set_from_installed(**kwargs):
|
||||
package_set[name] = PackageDetails(dist.version, dist.requires())
|
||||
except RequirementParseError as e:
|
||||
# Don't crash on broken metadata
|
||||
logging.warning("Error parsing requirements for %s: %s", name, e)
|
||||
logger.warning("Error parsing requirements for %s: %s", name, e)
|
||||
problems = True
|
||||
return package_set, problems
|
||||
|
||||
|
||||
@@ -80,7 +80,7 @@ def freeze(
|
||||
continue
|
||||
if exclude_editable and req.editable:
|
||||
continue
|
||||
installations[req.name] = req
|
||||
installations[req.canonical_name] = req
|
||||
|
||||
if requirement:
|
||||
# the options that don't get turned into an InstallRequirement
|
||||
@@ -139,22 +139,27 @@ def freeze(
|
||||
" (add #egg=PackageName to the URL to avoid"
|
||||
" this warning)"
|
||||
)
|
||||
elif line_req.name not in installations:
|
||||
# either it's not installed, or it is installed
|
||||
# but has been processed already
|
||||
if not req_files[line_req.name]:
|
||||
logger.warning(
|
||||
"Requirement file [%s] contains %s, but "
|
||||
"package %r is not installed",
|
||||
req_file_path,
|
||||
COMMENT_RE.sub('', line).strip(), line_req.name
|
||||
)
|
||||
else:
|
||||
req_files[line_req.name].append(req_file_path)
|
||||
else:
|
||||
yield str(installations[line_req.name]).rstrip()
|
||||
del installations[line_req.name]
|
||||
req_files[line_req.name].append(req_file_path)
|
||||
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
|
||||
if not req_files[line_req.name]:
|
||||
logger.warning(
|
||||
"Requirement file [%s] contains %s, but "
|
||||
"package %r is not installed",
|
||||
req_file_path,
|
||||
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()
|
||||
del installations[line_req_canonical_name]
|
||||
req_files[line_req.name].append(req_file_path)
|
||||
|
||||
# Warn about requirements that were included multiple times (in a
|
||||
# single requirements file or in different requirements files).
|
||||
@@ -169,7 +174,7 @@ def freeze(
|
||||
)
|
||||
for installation in sorted(
|
||||
installations.values(), key=lambda x: x.name.lower()):
|
||||
if canonicalize_name(installation.name) not in skip:
|
||||
if installation.canonical_name not in skip:
|
||||
yield str(installation).rstrip()
|
||||
|
||||
|
||||
@@ -239,6 +244,7 @@ class FrozenRequirement(object):
|
||||
def __init__(self, name, req, editable, comments=()):
|
||||
# type: (str, Union[str, Requirement], bool, Iterable[str]) -> None
|
||||
self.name = name
|
||||
self.canonical_name = canonicalize_name(name)
|
||||
self.req = req
|
||||
self.editable = editable
|
||||
self.comments = comments
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
"""For modules related to installing packages.
|
||||
"""
|
||||
@@ -0,0 +1,52 @@
|
||||
"""Legacy editable installation process, i.e. `setup.py develop`.
|
||||
"""
|
||||
import logging
|
||||
|
||||
from pipenv.patched.notpip._internal.utils.logging import indent_log
|
||||
from pipenv.patched.notpip._internal.utils.setuptools_build import make_setuptools_develop_args
|
||||
from pipenv.patched.notpip._internal.utils.subprocess import call_subprocess
|
||||
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
|
||||
|
||||
if MYPY_CHECK_RUNNING:
|
||||
from typing import List, Optional, Sequence
|
||||
|
||||
from pipenv.patched.notpip._internal.build_env import BuildEnvironment
|
||||
|
||||
|
||||
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 a package in editable mode. Most arguments are pass-through
|
||||
to setuptools.
|
||||
"""
|
||||
logger.info('Running setup.py develop for %s', name)
|
||||
|
||||
args = make_setuptools_develop_args(
|
||||
setup_py_path,
|
||||
global_options=global_options,
|
||||
install_options=install_options,
|
||||
no_user_config=isolated,
|
||||
prefix=prefix,
|
||||
home=home,
|
||||
use_user_site=use_user_site,
|
||||
)
|
||||
|
||||
with indent_log():
|
||||
with build_env:
|
||||
call_subprocess(
|
||||
args,
|
||||
cwd=unpacked_source_directory,
|
||||
)
|
||||
@@ -0,0 +1,129 @@
|
||||
"""Legacy installation process, i.e. `setup.py install`.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
from distutils.util import change_root
|
||||
|
||||
from pipenv.patched.notpip._internal.utils.deprecation import deprecated
|
||||
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
|
||||
from pipenv.patched.notpip._internal.utils.temp_dir import TempDirectory
|
||||
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
|
||||
|
||||
if MYPY_CHECK_RUNNING:
|
||||
from typing import List, Optional, Sequence
|
||||
|
||||
from pipenv.patched.notpip._internal.models.scheme import Scheme
|
||||
from pipenv.patched.notpip._internal.req.req_install import InstallRequirement
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def install(
|
||||
install_req, # type: InstallRequirement
|
||||
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
|
||||
):
|
||||
# type: (...) -> None
|
||||
# Extend the list of global and install options passed on to
|
||||
# the setup.py call with the ones from the requirements file.
|
||||
# Options specified in requirements file override those
|
||||
# specified on the command line, since the last option given
|
||||
# to setup.py is the one that is used.
|
||||
global_options = list(global_options) + \
|
||||
install_req.options.get('global_options', [])
|
||||
install_options = list(install_options) + \
|
||||
install_req.options.get('install_options', [])
|
||||
|
||||
header_dir = scheme.headers
|
||||
|
||||
with TempDirectory(kind="record") as temp_dir:
|
||||
record_filename = os.path.join(temp_dir.path, 'install-record.txt')
|
||||
install_args = make_setuptools_install_args(
|
||||
install_req.setup_py_path,
|
||||
global_options=global_options,
|
||||
install_options=install_options,
|
||||
record_filename=record_filename,
|
||||
root=root,
|
||||
prefix=prefix,
|
||||
header_dir=header_dir,
|
||||
home=home,
|
||||
use_user_site=use_user_site,
|
||||
no_user_config=install_req.isolated,
|
||||
pycompile=pycompile,
|
||||
)
|
||||
|
||||
runner = runner_with_spinner_message(
|
||||
"Running setup.py install for {}".format(install_req.name)
|
||||
)
|
||||
with indent_log(), install_req.build_env:
|
||||
runner(
|
||||
cmd=install_args,
|
||||
cwd=install_req.unpacked_source_directory,
|
||||
)
|
||||
|
||||
if not os.path.exists(record_filename):
|
||||
logger.debug('Record file %s not found', record_filename)
|
||||
return
|
||||
install_req.install_succeeded = True
|
||||
|
||||
# We intentionally do not use any encoding to read the file because
|
||||
# setuptools writes the file using distutils.file_util.write_file,
|
||||
# which does not specify an encoding.
|
||||
with open(record_filename) as f:
|
||||
record_lines = f.read().splitlines()
|
||||
|
||||
def prepend_root(path):
|
||||
# type: (str) -> str
|
||||
if root is None or not os.path.isabs(path):
|
||||
return path
|
||||
else:
|
||||
return change_root(root, path)
|
||||
|
||||
for line in record_lines:
|
||||
directory = os.path.dirname(line)
|
||||
if directory.endswith('.egg-info'):
|
||||
egg_info_dir = prepend_root(directory)
|
||||
break
|
||||
else:
|
||||
deprecated(
|
||||
reason=(
|
||||
"{} did not indicate that it installed an "
|
||||
".egg-info directory. Only setup.py projects "
|
||||
"generating .egg-info directories are supported."
|
||||
).format(install_req),
|
||||
replacement=(
|
||||
"for maintainers: updating the setup.py of {0}. "
|
||||
"For users: contact the maintainers of {0} to let "
|
||||
"them know to update their setup.py.".format(
|
||||
install_req.name
|
||||
)
|
||||
),
|
||||
gone_in="20.2",
|
||||
issue=6998,
|
||||
)
|
||||
# FIXME: put the record somewhere
|
||||
return
|
||||
new_lines = []
|
||||
for line in record_lines:
|
||||
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.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')
|
||||
@@ -0,0 +1,615 @@
|
||||
"""Support for installing and building the "wheel" binary package format.
|
||||
"""
|
||||
|
||||
# The following comment should be removed at some point in the future.
|
||||
# mypy: strict-optional=False
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import collections
|
||||
import compileall
|
||||
import csv
|
||||
import logging
|
||||
import os.path
|
||||
import re
|
||||
import shutil
|
||||
import stat
|
||||
import sys
|
||||
import warnings
|
||||
from base64 import urlsafe_b64encode
|
||||
from zipfile import ZipFile
|
||||
|
||||
from pipenv.patched.notpip._vendor import pkg_resources
|
||||
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.six import StringIO
|
||||
|
||||
from pipenv.patched.notpip._internal.exceptions import InstallationError
|
||||
from pipenv.patched.notpip._internal.locations import get_major_minor_version
|
||||
from pipenv.patched.notpip._internal.utils.misc import captured_stdout, ensure_dir, hash_file
|
||||
from pipenv.patched.notpip._internal.utils.temp_dir import TempDirectory
|
||||
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
|
||||
from pipenv.patched.notpip._internal.utils.unpacking import unpack_file
|
||||
from pipenv.patched.notpip._internal.utils.wheel import parse_wheel
|
||||
|
||||
if MYPY_CHECK_RUNNING:
|
||||
from email.message import Message
|
||||
from typing import (
|
||||
Dict, List, Optional, Sequence, Tuple, IO, Text, Any,
|
||||
Iterable, Callable, Set,
|
||||
)
|
||||
|
||||
from pipenv.patched.notpip._internal.models.scheme import Scheme
|
||||
|
||||
InstalledCSVRow = Tuple[str, ...]
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def normpath(src, p):
|
||||
# type: (str, str) -> str
|
||||
return os.path.relpath(src, p).replace(os.path.sep, '/')
|
||||
|
||||
|
||||
def rehash(path, blocksize=1 << 20):
|
||||
# type: (str, int) -> 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('=')
|
||||
# unicode/str python2 issues
|
||||
return (digest, str(length)) # type: ignore
|
||||
|
||||
|
||||
def open_for_csv(name, mode):
|
||||
# type: (str, Text) -> IO[Any]
|
||||
if sys.version_info[0] < 3:
|
||||
nl = {} # type: Dict[str, Any]
|
||||
bin = 'b'
|
||||
else:
|
||||
nl = {'newline': ''} # type: Dict[str, Any]
|
||||
bin = ''
|
||||
return open(name, mode + bin, **nl)
|
||||
|
||||
|
||||
def fix_script(path):
|
||||
# type: (str) -> Optional[bool]
|
||||
"""Replace #!python with #!/path/to/python
|
||||
Return True if file was changed.
|
||||
"""
|
||||
# XXX RECORD hashes will need to be updated
|
||||
if os.path.isfile(path):
|
||||
with open(path, 'rb') as script:
|
||||
firstline = script.readline()
|
||||
if not firstline.startswith(b'#!python'):
|
||||
return False
|
||||
exename = sys.executable.encode(sys.getfilesystemencoding())
|
||||
firstline = b'#!' + exename + os.linesep.encode("ascii")
|
||||
rest = script.read()
|
||||
with open(path, 'wb') as script:
|
||||
script.write(firstline)
|
||||
script.write(rest)
|
||||
return True
|
||||
return None
|
||||
|
||||
|
||||
def wheel_root_is_purelib(metadata):
|
||||
# type: (Message) -> bool
|
||||
return metadata.get("Root-Is-Purelib", "").lower() == "true"
|
||||
|
||||
|
||||
def get_entrypoints(filename):
|
||||
# type: (str) -> Tuple[Dict[str, str], Dict[str, str]]
|
||||
if not os.path.exists(filename):
|
||||
return {}, {}
|
||||
|
||||
# This is done because you can pass a string to entry_points wrappers which
|
||||
# means that they may or may not be valid INI files. The attempt here is to
|
||||
# strip leading and trailing whitespace in order to make them valid INI
|
||||
# files.
|
||||
with open(filename) as fp:
|
||||
data = StringIO()
|
||||
for line in fp:
|
||||
data.write(line.strip())
|
||||
data.write("\n")
|
||||
data.seek(0)
|
||||
|
||||
# get the entry points and then the script names
|
||||
entry_points = pkg_resources.EntryPoint.parse_map(data)
|
||||
console = entry_points.get('console_scripts', {})
|
||||
gui = entry_points.get('gui_scripts', {})
|
||||
|
||||
def _split_ep(s):
|
||||
# type: (pkg_resources.EntryPoint) -> Tuple[str, str]
|
||||
"""get the string representation of EntryPoint,
|
||||
remove space and split on '='
|
||||
"""
|
||||
split_parts = str(s).replace(" ", "").split("=")
|
||||
return split_parts[0], split_parts[1]
|
||||
|
||||
# convert the EntryPoint objects into strings with module:function
|
||||
console = dict(_split_ep(v) for v in console.values())
|
||||
gui = dict(_split_ep(v) for v in gui.values())
|
||||
return console, gui
|
||||
|
||||
|
||||
def message_about_scripts_not_on_PATH(scripts):
|
||||
# type: (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.
|
||||
"""
|
||||
if not scripts:
|
||||
return None
|
||||
|
||||
# Group scripts by the path they were installed in
|
||||
grouped_by_dir = collections.defaultdict(set) # type: Dict[str, Set[str]]
|
||||
for destfile in scripts:
|
||||
parent_dir = os.path.dirname(destfile)
|
||||
script_name = os.path.basename(destfile)
|
||||
grouped_by_dir[parent_dir].add(script_name)
|
||||
|
||||
# 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)
|
||||
]
|
||||
# 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()
|
||||
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]
|
||||
if len(sorted_scripts) == 1:
|
||||
start_text = "script {} is".format(sorted_scripts[0])
|
||||
else:
|
||||
start_text = "scripts {} are".format(
|
||||
", ".join(sorted_scripts[:-1]) + " and " + sorted_scripts[-1]
|
||||
)
|
||||
|
||||
msg_lines.append(
|
||||
"The {} installed in '{}' which is not on PATH."
|
||||
.format(start_text, parent_dir)
|
||||
)
|
||||
|
||||
last_line_fmt = (
|
||||
"Consider adding {} to PATH or, if you prefer "
|
||||
"to suppress this warning, use --no-warn-script-location."
|
||||
)
|
||||
if len(msg_lines) == 1:
|
||||
msg_lines.append(last_line_fmt.format("this directory"))
|
||||
else:
|
||||
msg_lines.append(last_line_fmt.format("these directories"))
|
||||
|
||||
# Add a note if any directory starts with ~
|
||||
warn_for_tilde = any(
|
||||
i[0] == "~" for i in os.environ.get("PATH", "").split(os.pathsep) if i
|
||||
)
|
||||
if warn_for_tilde:
|
||||
tilde_warning_msg = (
|
||||
"NOTE: The current PATH contains path(s) starting with `~`, "
|
||||
"which may not be expanded by all applications."
|
||||
)
|
||||
msg_lines.append(tilde_warning_msg)
|
||||
|
||||
# Returns the formatted multiline message
|
||||
return "\n".join(msg_lines)
|
||||
|
||||
|
||||
def sorted_outrows(outrows):
|
||||
# type: (Iterable[InstalledCSVRow]) -> List[InstalledCSVRow]
|
||||
"""Return the given rows of a RECORD file in sorted order.
|
||||
|
||||
Each row is a 3-tuple (path, hash, size) and corresponds to a record of
|
||||
a RECORD file (see PEP 376 and PEP 427 for details). For the rows
|
||||
passed to this function, the size can be an integer as an int or string,
|
||||
or the empty string.
|
||||
"""
|
||||
# Normally, there should only be one row per path, in which case the
|
||||
# second and third elements don't come into play when sorting.
|
||||
# However, in cases in the wild where a path might happen to occur twice,
|
||||
# we don't want the sort operation to trigger an error (but still want
|
||||
# determinism). Since the third element can be an int or string, we
|
||||
# coerce each element to a string to avoid a TypeError in this case.
|
||||
# For additional background, see--
|
||||
# https://github.com/pypa/pip/issues/5868
|
||||
return sorted(outrows, key=lambda row: tuple(str(x) for x in row))
|
||||
|
||||
|
||||
def get_csv_rows_for_installed(
|
||||
old_csv_rows, # type: Iterable[List[str]]
|
||||
installed, # type: Dict[str, str]
|
||||
changed, # type: Set[str]
|
||||
generated, # type: List[str]
|
||||
lib_dir, # type: str
|
||||
):
|
||||
# type: (...) -> List[InstalledCSVRow]
|
||||
"""
|
||||
:param installed: A map from archive RECORD path to installation RECORD
|
||||
path.
|
||||
"""
|
||||
installed_rows = [] # type: List[InstalledCSVRow]
|
||||
for row in old_csv_rows:
|
||||
if len(row) > 3:
|
||||
logger.warning(
|
||||
'RECORD line has more than three elements: {}'.format(row)
|
||||
)
|
||||
# Make a copy because we are mutating the row.
|
||||
row = list(row)
|
||||
old_path = row[0]
|
||||
new_path = installed.pop(old_path, old_path)
|
||||
row[0] = new_path
|
||||
if new_path in changed:
|
||||
digest, length = rehash(new_path)
|
||||
row[1] = digest
|
||||
row[2] = length
|
||||
installed_rows.append(tuple(row))
|
||||
for f in generated:
|
||||
digest, length = rehash(f)
|
||||
installed_rows.append((normpath(f, lib_dir), digest, str(length)))
|
||||
for f in installed:
|
||||
installed_rows.append((installed[f], '', ''))
|
||||
return installed_rows
|
||||
|
||||
|
||||
class MissingCallableSuffix(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def _raise_for_invalid_entrypoint(specification):
|
||||
# type: (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]
|
||||
_raise_for_invalid_entrypoint(specification)
|
||||
return super(PipScriptMaker, self).make(specification, options)
|
||||
|
||||
|
||||
def install_unpacked_wheel(
|
||||
name, # type: str
|
||||
wheeldir, # type: str
|
||||
wheel_zip, # type: ZipFile
|
||||
scheme, # type: Scheme
|
||||
req_description, # type: str
|
||||
pycompile=True, # type: bool
|
||||
warn_script_location=True # type: bool
|
||||
):
|
||||
# type: (...) -> None
|
||||
"""Install a wheel.
|
||||
|
||||
:param name: Name of the project to install
|
||||
:param wheeldir: Base directory of the unpacked wheel
|
||||
:param wheel_zip: open ZipFile for wheel being installed
|
||||
:param scheme: Distutils scheme dictating the install directories
|
||||
:param req_description: String used in place of the requirement, for
|
||||
logging
|
||||
:param pycompile: Whether to byte-compile installed Python files
|
||||
:param warn_script_location: Whether to check that scripts are installed
|
||||
into a directory on PATH
|
||||
:raises UnsupportedWheel:
|
||||
* when the directory holds an unpacked wheel with incompatible
|
||||
Wheel-Version
|
||||
* when the .dist-info dir does not match the wheel
|
||||
"""
|
||||
# TODO: Investigate and break this up.
|
||||
# TODO: Look into moving this into a dedicated class for representing an
|
||||
# installation.
|
||||
|
||||
source = wheeldir.rstrip(os.path.sep) + os.path.sep
|
||||
|
||||
info_dir, metadata = parse_wheel(wheel_zip, name)
|
||||
|
||||
if wheel_root_is_purelib(metadata):
|
||||
lib_dir = scheme.purelib
|
||||
else:
|
||||
lib_dir = scheme.platlib
|
||||
|
||||
subdirs = os.listdir(source)
|
||||
data_dirs = [s for s in subdirs if s.endswith('.data')]
|
||||
|
||||
# Record details of the files moved
|
||||
# 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[str, str]
|
||||
changed = set()
|
||||
generated = [] # type: List[str]
|
||||
|
||||
# Compile all of the pyc files that we're going to be installing
|
||||
if pycompile:
|
||||
with captured_stdout() as stdout:
|
||||
with warnings.catch_warnings():
|
||||
warnings.filterwarnings('ignore')
|
||||
compileall.compile_dir(source, force=True, quiet=True)
|
||||
logger.debug(stdout.getvalue())
|
||||
|
||||
def record_installed(srcfile, destfile, modified=False):
|
||||
# type: (str, str, bool) -> None
|
||||
"""Map archive RECORD paths to installation RECORD paths."""
|
||||
oldpath = normpath(srcfile, wheeldir)
|
||||
newpath = normpath(destfile, lib_dir)
|
||||
installed[oldpath] = newpath
|
||||
if modified:
|
||||
changed.add(destfile)
|
||||
|
||||
def clobber(
|
||||
source, # type: str
|
||||
dest, # type: str
|
||||
is_base, # type: bool
|
||||
fixer=None, # type: Optional[Callable[[str], Any]]
|
||||
filter=None # type: Optional[Callable[[str], bool]]
|
||||
):
|
||||
# type: (...) -> None
|
||||
ensure_dir(dest) # common for the 'include' path
|
||||
|
||||
for dir, subdirs, files in os.walk(source):
|
||||
basedir = dir[len(source):].lstrip(os.path.sep)
|
||||
destdir = os.path.join(dest, basedir)
|
||||
if is_base and basedir == '':
|
||||
subdirs[:] = [s for s in subdirs if not s.endswith('.data')]
|
||||
for f in files:
|
||||
# Skip unwanted files
|
||||
if filter and filter(f):
|
||||
continue
|
||||
srcfile = os.path.join(dir, f)
|
||||
destfile = os.path.join(dest, basedir, f)
|
||||
# directory creation is lazy and after the file filtering above
|
||||
# to ensure we don't install empty dirs; empty dirs can't be
|
||||
# uninstalled.
|
||||
ensure_dir(destdir)
|
||||
|
||||
# copyfile (called below) truncates the destination if it
|
||||
# exists and then writes the new contents. This is fine in most
|
||||
# cases, but can cause a segfault if pip has loaded a shared
|
||||
# object (e.g. from pyopenssl through its vendored urllib3)
|
||||
# Since the shared object is mmap'd an attempt to call a
|
||||
# symbol in it will then cause a segfault. Unlinking the file
|
||||
# allows writing of new contents while allowing the process to
|
||||
# continue to use the old copy.
|
||||
if os.path.exists(destfile):
|
||||
os.unlink(destfile)
|
||||
|
||||
# We use copyfile (not move, copy, or copy2) to be extra sure
|
||||
# that we are not moving directories over (copyfile fails for
|
||||
# directories) as well as to ensure that we are not copying
|
||||
# over any metadata because we want more control over what
|
||||
# metadata we actually copy over.
|
||||
shutil.copyfile(srcfile, destfile)
|
||||
|
||||
# Copy over the metadata for the file, currently this only
|
||||
# includes the atime and mtime.
|
||||
st = os.stat(srcfile)
|
||||
if hasattr(os, "utime"):
|
||||
os.utime(destfile, (st.st_atime, st.st_mtime))
|
||||
|
||||
# If our file is executable, then make our destination file
|
||||
# executable.
|
||||
if os.access(srcfile, os.X_OK):
|
||||
st = os.stat(srcfile)
|
||||
permissions = (
|
||||
st.st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
|
||||
)
|
||||
os.chmod(destfile, permissions)
|
||||
|
||||
changed = False
|
||||
if fixer:
|
||||
changed = fixer(destfile)
|
||||
record_installed(srcfile, destfile, changed)
|
||||
|
||||
clobber(source, lib_dir, True)
|
||||
|
||||
dest_info_dir = os.path.join(lib_dir, info_dir)
|
||||
|
||||
# Get the defined entry points
|
||||
ep_file = os.path.join(dest_info_dir, 'entry_points.txt')
|
||||
console, gui = get_entrypoints(ep_file)
|
||||
|
||||
def is_entrypoint_wrapper(name):
|
||||
# type: (str) -> bool
|
||||
# EP, EP.exe and EP-script.py are scripts generated for
|
||||
# entry point EP by setuptools
|
||||
if name.lower().endswith('.exe'):
|
||||
matchname = name[:-4]
|
||||
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)
|
||||
|
||||
for datadir in data_dirs:
|
||||
fixer = None
|
||||
filter = None
|
||||
for subdir in os.listdir(os.path.join(wheeldir, datadir)):
|
||||
fixer = None
|
||||
if subdir == 'scripts':
|
||||
fixer = fix_script
|
||||
filter = is_entrypoint_wrapper
|
||||
source = os.path.join(wheeldir, datadir, subdir)
|
||||
dest = getattr(scheme, subdir)
|
||||
clobber(source, dest, False, fixer=fixer, filter=filter)
|
||||
|
||||
maker = PipScriptMaker(None, scheme.scripts)
|
||||
|
||||
# Ensure old scripts are overwritten.
|
||||
# See https://github.com/pypa/pip/issues/1800
|
||||
maker.clobber = True
|
||||
|
||||
# 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 = {''}
|
||||
|
||||
# This is required because otherwise distlib creates scripts that are not
|
||||
# executable.
|
||||
# See https://bitbucket.org/pypa/distlib/issue/32/
|
||||
maker.set_mode = True
|
||||
|
||||
scripts_to_generate = []
|
||||
|
||||
# Special case pip and setuptools to generate versioned wrappers
|
||||
#
|
||||
# The issue is that some projects (specifically, pip and setuptools) use
|
||||
# code in setup.py to create "versioned" entry points - pip2.7 on Python
|
||||
# 2.7, pip3.3 on Python 3.3, etc. But these entry points are baked into
|
||||
# the wheel metadata at build time, and so if the wheel is installed with
|
||||
# a *different* version of Python the entry points will be wrong. The
|
||||
# correct fix for this is to enhance the metadata to be able to describe
|
||||
# such versioned entry points, but that won't happen till Metadata 2.0 is
|
||||
# available.
|
||||
# In the meantime, projects using versioned entry points will either have
|
||||
# incorrect versioned entry points, or they will not be able to distribute
|
||||
# "universal" wheels (i.e., they will need a wheel per Python version).
|
||||
#
|
||||
# Because setuptools and pip are bundled with _ensurepip and virtualenv,
|
||||
# we need to use universal wheels. So, as a stopgap until Metadata 2.0, we
|
||||
# override the versioned entry points in the wheel and generate the
|
||||
# correct ones. This code is purely a short-term measure until Metadata 2.0
|
||||
# is available.
|
||||
#
|
||||
# To add the level of hack in this section of code, in order to support
|
||||
# ensurepip this code will look for an ``ENSUREPIP_OPTIONS`` environment
|
||||
# variable which will control which version scripts get installed.
|
||||
#
|
||||
# ENSUREPIP_OPTIONS=altinstall
|
||||
# - Only pipX.Y and easy_install-X.Y will be generated and installed
|
||||
# ENSUREPIP_OPTIONS=install
|
||||
# - pipX.Y, pipX, easy_install-X.Y will be generated and installed. Note
|
||||
# that this option is technically if ENSUREPIP_OPTIONS is set and is
|
||||
# not altinstall
|
||||
# DEFAULT
|
||||
# - The default behavior is to install pip, pipX, pipX.Y, easy_install
|
||||
# and easy_install-X.Y.
|
||||
pip_script = console.pop('pip', None)
|
||||
if pip_script:
|
||||
if "ENSUREPIP_OPTIONS" not in os.environ:
|
||||
scripts_to_generate.append('pip = ' + pip_script)
|
||||
|
||||
if os.environ.get("ENSUREPIP_OPTIONS", "") != "altinstall":
|
||||
scripts_to_generate.append(
|
||||
'pip%s = %s' % (sys.version_info[0], pip_script)
|
||||
)
|
||||
|
||||
scripts_to_generate.append(
|
||||
'pip%s = %s' % (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)]
|
||||
for k in pip_ep:
|
||||
del console[k]
|
||||
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-%s = %s' % (
|
||||
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)
|
||||
]
|
||||
for k in easy_install_ep:
|
||||
del console[k]
|
||||
|
||||
# Generate the console and GUI entry points specified in the wheel
|
||||
scripts_to_generate.extend(
|
||||
'%s = %s' % kv for kv in console.items()
|
||||
)
|
||||
|
||||
gui_scripts_to_generate = [
|
||||
'%s = %s' % kv for kv in gui.items()
|
||||
]
|
||||
|
||||
generated_console_scripts = [] # type: List[str]
|
||||
|
||||
try:
|
||||
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})
|
||||
)
|
||||
except MissingCallableSuffix as e:
|
||||
entry = e.args[0]
|
||||
raise InstallationError(
|
||||
"Invalid script entry point: {} for req: {} - A callable "
|
||||
"suffix is required. Cf https://packaging.python.org/"
|
||||
"specifications/entry-points/#use-for-scripts for more "
|
||||
"information.".format(entry, req_description)
|
||||
)
|
||||
|
||||
if warn_script_location:
|
||||
msg = message_about_scripts_not_on_PATH(generated_console_scripts)
|
||||
if msg is not None:
|
||||
logger.warning(msg)
|
||||
|
||||
# Record pip as the installer
|
||||
installer = os.path.join(dest_info_dir, 'INSTALLER')
|
||||
temp_installer = os.path.join(dest_info_dir, 'INSTALLER.pip')
|
||||
with open(temp_installer, 'wb') as installer_file:
|
||||
installer_file.write(b'pip\n')
|
||||
shutil.move(temp_installer, installer)
|
||||
generated.append(installer)
|
||||
|
||||
# Record details of all files installed
|
||||
record = os.path.join(dest_info_dir, 'RECORD')
|
||||
temp_record = os.path.join(dest_info_dir, 'RECORD.pip')
|
||||
with open_for_csv(record, 'r') as record_in:
|
||||
with open_for_csv(temp_record, 'w+') as record_out:
|
||||
reader = csv.reader(record_in)
|
||||
outrows = get_csv_rows_for_installed(
|
||||
reader, installed=installed, changed=changed,
|
||||
generated=generated, lib_dir=lib_dir,
|
||||
)
|
||||
writer = csv.writer(record_out)
|
||||
# Sort to simplify testing.
|
||||
for row in sorted_outrows(outrows):
|
||||
writer.writerow(row)
|
||||
shutil.move(temp_record, record)
|
||||
|
||||
|
||||
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
|
||||
_temp_dir_for_testing=None, # type: Optional[str]
|
||||
):
|
||||
# type: (...) -> None
|
||||
with TempDirectory(
|
||||
path=_temp_dir_for_testing, kind="unpacked-wheel"
|
||||
) as unpacked_dir, ZipFile(wheel_path, allowZip64=True) as z:
|
||||
unpack_file(wheel_path, unpacked_dir.path)
|
||||
install_unpacked_wheel(
|
||||
name=name,
|
||||
wheeldir=unpacked_dir.path,
|
||||
wheel_zip=z,
|
||||
scheme=scheme,
|
||||
req_description=req_description,
|
||||
pycompile=pycompile,
|
||||
warn_script_location=warn_script_location,
|
||||
)
|
||||
@@ -3,45 +3,91 @@
|
||||
|
||||
# The following comment should be removed at some point in the future.
|
||||
# mypy: strict-optional=False
|
||||
# mypy: disallow-untyped-defs=False
|
||||
|
||||
import logging
|
||||
import mimetypes
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
from pipenv.patched.notpip._vendor import requests
|
||||
from pipenv.patched.notpip._vendor.six import PY2
|
||||
|
||||
from pipenv.patched.notpip._internal.distributions import (
|
||||
make_distribution_for_install_requirement,
|
||||
)
|
||||
from pipenv.patched.notpip._internal.distributions.installed import InstalledDistribution
|
||||
from pipenv.patched.notpip._internal.download import unpack_url
|
||||
from pipenv.patched.notpip._internal.exceptions import (
|
||||
DirectoryUrlHashUnsupported,
|
||||
HashMismatch,
|
||||
HashUnpinned,
|
||||
InstallationError,
|
||||
PreviousBuildDirError,
|
||||
VcsHashUnsupported,
|
||||
)
|
||||
from pipenv.patched.notpip._internal.utils.compat import expanduser
|
||||
from pipenv.patched.notpip._internal.utils.filesystem import copy2_fixed
|
||||
from pipenv.patched.notpip._internal.utils.hashes import MissingHashes
|
||||
from pipenv.patched.notpip._internal.utils.logging import indent_log
|
||||
from pipenv.patched.notpip._internal.utils.marker_files import write_delete_marker_file
|
||||
from pipenv.patched.notpip._internal.utils.misc import display_path, normalize_path
|
||||
from pipenv.patched.notpip._internal.utils.misc import (
|
||||
ask_path_exists,
|
||||
backup_dir,
|
||||
display_path,
|
||||
hide_url,
|
||||
path_to_display,
|
||||
rmtree,
|
||||
)
|
||||
from pipenv.patched.notpip._internal.utils.temp_dir import TempDirectory
|
||||
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
|
||||
from pipenv.patched.notpip._internal.utils.unpacking import unpack_file
|
||||
from pipenv.patched.notpip._internal.vcs import vcs
|
||||
|
||||
if MYPY_CHECK_RUNNING:
|
||||
from typing import Optional
|
||||
from typing import (
|
||||
Callable, List, Optional, Tuple,
|
||||
)
|
||||
|
||||
from mypy_extensions import TypedDict
|
||||
|
||||
from pipenv.patched.notpip._internal.distributions import AbstractDistribution
|
||||
from pipenv.patched.notpip._internal.index import PackageFinder
|
||||
from pipenv.patched.notpip._internal.network.session import PipSession
|
||||
from pipenv.patched.notpip._internal.index.package_finder import PackageFinder
|
||||
from pipenv.patched.notpip._internal.models.link import Link
|
||||
from pipenv.patched.notpip._internal.network.download import Downloader
|
||||
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.hashes import Hashes
|
||||
|
||||
if PY2:
|
||||
CopytreeKwargs = TypedDict(
|
||||
'CopytreeKwargs',
|
||||
{
|
||||
'ignore': Callable[[str, List[str]], List[str]],
|
||||
'symlinks': bool,
|
||||
},
|
||||
total=False,
|
||||
)
|
||||
else:
|
||||
CopytreeKwargs = TypedDict(
|
||||
'CopytreeKwargs',
|
||||
{
|
||||
'copy_function': Callable[[str, str], None],
|
||||
'ignore': Callable[[str, List[str]], List[str]],
|
||||
'ignore_dangling_symlinks': bool,
|
||||
'symlinks': bool,
|
||||
},
|
||||
total=False,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _get_prepared_distribution(req, req_tracker, finder, build_isolation):
|
||||
def _get_prepared_distribution(
|
||||
req, # type: InstallRequirement
|
||||
req_tracker, # type: RequirementTracker
|
||||
finder, # type: PackageFinder
|
||||
build_isolation # type: bool
|
||||
):
|
||||
# type: (...) -> AbstractDistribution
|
||||
"""Prepare a distribution for installation.
|
||||
"""
|
||||
abstract_dist = make_distribution_for_install_requirement(req)
|
||||
@@ -50,6 +96,245 @@ def _get_prepared_distribution(req, req_tracker, finder, build_isolation):
|
||||
return abstract_dist
|
||||
|
||||
|
||||
def unpack_vcs_link(link, location):
|
||||
# type: (Link, str) -> 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))
|
||||
|
||||
|
||||
def _copy_file(filename, location, link):
|
||||
# type: (str, str, Link) -> None
|
||||
copy = True
|
||||
download_location = os.path.join(location, link.filename)
|
||||
if os.path.exists(download_location):
|
||||
response = ask_path_exists(
|
||||
'The file {} exists. (i)gnore, (w)ipe, (b)ackup, (a)abort'.format(
|
||||
display_path(download_location)
|
||||
),
|
||||
('i', 'w', 'b', 'a'),
|
||||
)
|
||||
if response == 'i':
|
||||
copy = False
|
||||
elif response == 'w':
|
||||
logger.warning('Deleting %s', display_path(download_location))
|
||||
os.remove(download_location)
|
||||
elif response == 'b':
|
||||
dest_file = backup_dir(download_location)
|
||||
logger.warning(
|
||||
'Backing up %s to %s',
|
||||
display_path(download_location),
|
||||
display_path(dest_file),
|
||||
)
|
||||
shutil.move(download_location, dest_file)
|
||||
elif response == 'a':
|
||||
sys.exit(-1)
|
||||
if copy:
|
||||
shutil.copy(filename, download_location)
|
||||
logger.info('Saved %s', display_path(download_location))
|
||||
|
||||
|
||||
def unpack_http_url(
|
||||
link, # type: Link
|
||||
location, # type: str
|
||||
downloader, # type: Downloader
|
||||
download_dir=None, # type: Optional[str]
|
||||
hashes=None, # type: Optional[Hashes]
|
||||
):
|
||||
# type: (...) -> str
|
||||
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
|
||||
)
|
||||
|
||||
if already_downloaded_path:
|
||||
from_path = already_downloaded_path
|
||||
content_type = mimetypes.guess_type(from_path)[0]
|
||||
else:
|
||||
# let's download to a tmp dir
|
||||
from_path, content_type = _download_http_url(
|
||||
link, downloader, temp_dir.path, hashes
|
||||
)
|
||||
|
||||
# unpack the archive to the build dir location. even when only
|
||||
# downloading archives, they have to be unpacked to parse dependencies
|
||||
unpack_file(from_path, location, content_type)
|
||||
|
||||
return from_path
|
||||
|
||||
|
||||
def _copy2_ignoring_special_files(src, dest):
|
||||
# type: (str, 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.
|
||||
"""
|
||||
try:
|
||||
copy2_fixed(src, dest)
|
||||
except shutil.SpecialFileError as e:
|
||||
# SpecialFileError may be raised due to either the source or
|
||||
# destination. If the destination was the cause then we would actually
|
||||
# care, but since the destination directory is deleted prior to
|
||||
# copy we ignore all of them assuming it is caused by the source.
|
||||
logger.warning(
|
||||
"Ignoring special file error '%s' encountered copying %s to %s.",
|
||||
str(e),
|
||||
path_to_display(src),
|
||||
path_to_display(dest),
|
||||
)
|
||||
|
||||
|
||||
def _copy_source_tree(source, target):
|
||||
# type: (str, str) -> None
|
||||
def ignore(d, names):
|
||||
# type: (str, List[str]) -> List[str]
|
||||
# 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
|
||||
return ['.tox', '.nox'] if d == source else []
|
||||
|
||||
kwargs = dict(ignore=ignore, symlinks=True) # type: CopytreeKwargs
|
||||
|
||||
if not PY2:
|
||||
# Python 2 does not support copy_function, so we only ignore
|
||||
# errors on special file copy in Python 3.
|
||||
kwargs['copy_function'] = _copy2_ignoring_special_files
|
||||
|
||||
shutil.copytree(source, target, **kwargs)
|
||||
|
||||
|
||||
def unpack_file_url(
|
||||
link, # type: Link
|
||||
location, # type: str
|
||||
download_dir=None, # type: Optional[str]
|
||||
hashes=None # type: Optional[Hashes]
|
||||
):
|
||||
# type: (...) -> Optional[str]
|
||||
"""Unpack link into location.
|
||||
"""
|
||||
link_path = link.file_path
|
||||
# If it's a url to a local directory
|
||||
if link.is_existing_dir():
|
||||
if os.path.isdir(location):
|
||||
rmtree(location)
|
||||
_copy_source_tree(link_path, location)
|
||||
return None
|
||||
|
||||
# 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
|
||||
)
|
||||
|
||||
if already_downloaded_path:
|
||||
from_path = already_downloaded_path
|
||||
else:
|
||||
from_path = link_path
|
||||
|
||||
# If --require-hashes is off, `hashes` is either empty, the
|
||||
# link's embedded hash, or MissingHashes; it is required to
|
||||
# match. If --require-hashes is on, we are satisfied by any
|
||||
# hash in `hashes` matching: a URL-based or an option-based
|
||||
# one; no internet-sourced hash will be in `hashes`.
|
||||
if hashes:
|
||||
hashes.check_against_path(from_path)
|
||||
|
||||
content_type = mimetypes.guess_type(from_path)[0]
|
||||
|
||||
# unpack the archive to the build dir location. even when only downloading
|
||||
# archives, they have to be unpacked to parse dependencies
|
||||
unpack_file(from_path, location, content_type)
|
||||
|
||||
return from_path
|
||||
|
||||
|
||||
def unpack_url(
|
||||
link, # type: Link
|
||||
location, # type: str
|
||||
downloader, # type: Downloader
|
||||
download_dir=None, # type: Optional[str]
|
||||
hashes=None, # type: Optional[Hashes]
|
||||
):
|
||||
# type: (...) -> Optional[str]
|
||||
"""Unpack link into location, downloading if required.
|
||||
|
||||
:param hashes: A Hashes object, one of whose embedded hashes must match,
|
||||
or HashMismatch will be raised. If the Hashes is empty, no matches are
|
||||
required, and unhashable types of requirements (like VCS ones, which
|
||||
would ordinarily raise HashUnsupported) are allowed.
|
||||
"""
|
||||
# non-editable vcs urls
|
||||
if link.is_vcs:
|
||||
unpack_vcs_link(link, location)
|
||||
return None
|
||||
|
||||
# file urls
|
||||
elif link.is_file:
|
||||
return unpack_file_url(link, location, download_dir, hashes=hashes)
|
||||
|
||||
# http urls
|
||||
else:
|
||||
return unpack_http_url(
|
||||
link,
|
||||
location,
|
||||
downloader,
|
||||
download_dir,
|
||||
hashes=hashes,
|
||||
)
|
||||
|
||||
|
||||
def _download_http_url(
|
||||
link, # type: Link
|
||||
downloader, # type: Downloader
|
||||
temp_dir, # type: str
|
||||
hashes, # type: Optional[Hashes]
|
||||
):
|
||||
# type: (...) -> Tuple[str, str]
|
||||
"""Download link url into temp_dir using provided session"""
|
||||
download = downloader(link)
|
||||
|
||||
file_path = os.path.join(temp_dir, download.filename)
|
||||
with open(file_path, 'wb') as content_file:
|
||||
for chunk in download.chunks:
|
||||
content_file.write(chunk)
|
||||
|
||||
if hashes:
|
||||
hashes.check_against_path(file_path)
|
||||
|
||||
return file_path, download.response.headers.get('content-type', '')
|
||||
|
||||
|
||||
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
|
||||
"""
|
||||
download_path = os.path.join(download_dir, link.filename)
|
||||
|
||||
if not os.path.exists(download_path):
|
||||
return None
|
||||
|
||||
# If already downloaded, does its hash match?
|
||||
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
|
||||
)
|
||||
os.unlink(download_path)
|
||||
return None
|
||||
return download_path
|
||||
|
||||
|
||||
class RequirementPreparer(object):
|
||||
"""Prepares a Requirement
|
||||
"""
|
||||
@@ -60,9 +345,12 @@ class RequirementPreparer(object):
|
||||
download_dir, # type: Optional[str]
|
||||
src_dir, # type: str
|
||||
wheel_download_dir, # type: Optional[str]
|
||||
progress_bar, # type: str
|
||||
build_isolation, # type: bool
|
||||
req_tracker # type: RequirementTracker
|
||||
req_tracker, # type: RequirementTracker
|
||||
downloader, # type: Downloader
|
||||
finder, # type: PackageFinder
|
||||
require_hashes, # type: bool
|
||||
use_user_site, # type: bool
|
||||
):
|
||||
# type: (...) -> None
|
||||
super(RequirementPreparer, self).__init__()
|
||||
@@ -70,18 +358,16 @@ class RequirementPreparer(object):
|
||||
self.src_dir = src_dir
|
||||
self.build_dir = build_dir
|
||||
self.req_tracker = req_tracker
|
||||
self.downloader = downloader
|
||||
self.finder = finder
|
||||
|
||||
# Where still-packed archives should be written to. If None, they are
|
||||
# not saved, and are deleted immediately after unpacking.
|
||||
if download_dir:
|
||||
download_dir = expanduser(download_dir)
|
||||
self.download_dir = download_dir
|
||||
|
||||
# Where still-packed .whl files should be written to. If None, they are
|
||||
# written to the download_dir parameter. Separate to download_dir to
|
||||
# permit only keeping wheel archives for pip wheel.
|
||||
if wheel_download_dir:
|
||||
wheel_download_dir = normalize_path(wheel_download_dir)
|
||||
self.wheel_download_dir = wheel_download_dir
|
||||
|
||||
# NOTE
|
||||
@@ -89,11 +375,15 @@ class RequirementPreparer(object):
|
||||
# be combined if we're willing to have non-wheel archives present in
|
||||
# the wheelhouse output by 'pip wheel'.
|
||||
|
||||
self.progress_bar = progress_bar
|
||||
|
||||
# Is build isolation allowed?
|
||||
self.build_isolation = build_isolation
|
||||
|
||||
# Should hash-checking be required?
|
||||
self.require_hashes = require_hashes
|
||||
|
||||
# Should install in user site-packages?
|
||||
self.use_user_site = use_user_site
|
||||
|
||||
@property
|
||||
def _download_should_save(self):
|
||||
# type: () -> bool
|
||||
@@ -105,15 +395,12 @@ class RequirementPreparer(object):
|
||||
|
||||
logger.critical('Could not find download directory')
|
||||
raise InstallationError(
|
||||
"Could not find or access download directory '%s'"
|
||||
% display_path(self.download_dir))
|
||||
"Could not find or access download directory '{}'"
|
||||
.format(self.download_dir))
|
||||
|
||||
def prepare_linked_requirement(
|
||||
self,
|
||||
req, # type: InstallRequirement
|
||||
session, # type: PipSession
|
||||
finder, # type: PackageFinder
|
||||
require_hashes, # type: bool
|
||||
):
|
||||
# type: (...) -> AbstractDistribution
|
||||
"""Prepare a requirement that would be obtained from req.link
|
||||
@@ -133,6 +420,8 @@ class RequirementPreparer(object):
|
||||
# editable in a req, a non deterministic error
|
||||
# occurs when the script attempts to unpack the
|
||||
# build directory
|
||||
# Since source_dir is only set for editable requirements.
|
||||
assert req.source_dir is None
|
||||
req.ensure_has_source_dir(self.build_dir)
|
||||
# If a checkout exists, it's unwise to keep going. version
|
||||
# inconsistencies are logged later, but do not fail the
|
||||
@@ -146,7 +435,7 @@ class RequirementPreparer(object):
|
||||
# requirements we have and raise some more informative errors
|
||||
# than otherwise. (For example, we can raise VcsHashUnsupported
|
||||
# for a VCS URL rather than HashMissing.)
|
||||
if require_hashes:
|
||||
if self.require_hashes:
|
||||
# We could check these first 2 conditions inside
|
||||
# unpack_url and save repetition of conditions, but then
|
||||
# we would report less-useful error messages for
|
||||
@@ -166,8 +455,8 @@ class RequirementPreparer(object):
|
||||
# about them not being pinned.
|
||||
raise HashUnpinned()
|
||||
|
||||
hashes = req.hashes(trust_internet=not require_hashes)
|
||||
if require_hashes and not hashes:
|
||||
hashes = req.hashes(trust_internet=not self.require_hashes)
|
||||
if self.require_hashes and not hashes:
|
||||
# Known-good hashes are missing for this requirement, so
|
||||
# shim it with a facade object that will provoke hash
|
||||
# computation and then raise a HashMissing exception
|
||||
@@ -181,10 +470,9 @@ class RequirementPreparer(object):
|
||||
download_dir = self.wheel_download_dir
|
||||
|
||||
try:
|
||||
unpack_url(
|
||||
link, req.source_dir, download_dir,
|
||||
session=session, hashes=hashes,
|
||||
progress_bar=self.progress_bar
|
||||
local_path = unpack_url(
|
||||
link, req.source_dir, self.downloader, download_dir,
|
||||
hashes=hashes,
|
||||
)
|
||||
except requests.HTTPError as exc:
|
||||
logger.critical(
|
||||
@@ -193,11 +481,15 @@ class RequirementPreparer(object):
|
||||
exc,
|
||||
)
|
||||
raise InstallationError(
|
||||
'Could not install requirement %s because of HTTP '
|
||||
'error %s for URL %s' %
|
||||
(req, exc, link)
|
||||
'Could not install requirement {} because of HTTP '
|
||||
'error {} for URL {}'.format(req, exc, link)
|
||||
)
|
||||
|
||||
# For use in later processing, preserve the file path on the
|
||||
# requirement.
|
||||
if local_path:
|
||||
req.local_file_path = local_path
|
||||
|
||||
if link.is_wheel:
|
||||
if download_dir:
|
||||
# When downloading, we only unpack wheels to get
|
||||
@@ -214,9 +506,17 @@ class RequirementPreparer(object):
|
||||
write_delete_marker_file(req.source_dir)
|
||||
|
||||
abstract_dist = _get_prepared_distribution(
|
||||
req, self.req_tracker, finder, self.build_isolation,
|
||||
req, self.req_tracker, self.finder, self.build_isolation,
|
||||
)
|
||||
|
||||
if download_dir:
|
||||
if link.is_existing_dir():
|
||||
logger.info('Link is a directory, ignoring download_dir')
|
||||
elif local_path and not os.path.exists(
|
||||
os.path.join(download_dir, link.filename)
|
||||
):
|
||||
_copy_file(local_path, download_dir, link)
|
||||
|
||||
if self._download_should_save:
|
||||
# Make a .zip of the source_dir we already created.
|
||||
if link.is_vcs:
|
||||
@@ -226,9 +526,6 @@ class RequirementPreparer(object):
|
||||
def prepare_editable_requirement(
|
||||
self,
|
||||
req, # type: InstallRequirement
|
||||
require_hashes, # type: bool
|
||||
use_user_site, # type: bool
|
||||
finder # type: PackageFinder
|
||||
):
|
||||
# type: (...) -> AbstractDistribution
|
||||
"""Prepare an editable requirement
|
||||
@@ -238,29 +535,28 @@ class RequirementPreparer(object):
|
||||
logger.info('Obtaining %s', req)
|
||||
|
||||
with indent_log():
|
||||
if require_hashes:
|
||||
if self.require_hashes:
|
||||
raise InstallationError(
|
||||
'The editable requirement %s cannot be installed when '
|
||||
'The editable requirement {} cannot be installed when '
|
||||
'requiring hashes, because there is no single file to '
|
||||
'hash.' % req
|
||||
'hash.'.format(req)
|
||||
)
|
||||
req.ensure_has_source_dir(self.src_dir)
|
||||
req.update_editable(not self._download_should_save)
|
||||
|
||||
abstract_dist = _get_prepared_distribution(
|
||||
req, self.req_tracker, finder, self.build_isolation,
|
||||
req, self.req_tracker, self.finder, self.build_isolation,
|
||||
)
|
||||
|
||||
if self._download_should_save:
|
||||
req.archive(self.download_dir)
|
||||
req.check_if_exists(use_user_site)
|
||||
req.check_if_exists(self.use_user_site)
|
||||
|
||||
return abstract_dist
|
||||
|
||||
def prepare_installed_requirement(
|
||||
self,
|
||||
req, # type: InstallRequirement
|
||||
require_hashes, # type: bool
|
||||
skip_reason # type: str
|
||||
):
|
||||
# type: (...) -> AbstractDistribution
|
||||
@@ -269,14 +565,14 @@ class RequirementPreparer(object):
|
||||
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 %r" % (req.satisfied_by,)
|
||||
"is set to {}".format(req.satisfied_by)
|
||||
)
|
||||
logger.info(
|
||||
'Requirement %s: %s (%s)',
|
||||
skip_reason, req, req.satisfied_by.version
|
||||
)
|
||||
with indent_log():
|
||||
if require_hashes:
|
||||
if self.require_hashes:
|
||||
logger.debug(
|
||||
'Since it is already installed, we are trusting this '
|
||||
'package without checking its hash. To ensure a '
|
||||
|
||||
@@ -1,345 +1,121 @@
|
||||
"""Generate and work with PEP 425 Compatibility Tags."""
|
||||
from __future__ import absolute_import
|
||||
|
||||
import distutils.util
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import sys
|
||||
import sysconfig
|
||||
import warnings
|
||||
from collections import OrderedDict
|
||||
|
||||
import pipenv.patched.notpip._internal.utils.glibc
|
||||
from pipenv.patched.notpip._internal.utils.compat import get_extension_suffixes
|
||||
from pipenv.patched.notpip._vendor.packaging.tags import (
|
||||
Tag,
|
||||
compatible_tags,
|
||||
cpython_tags,
|
||||
generic_tags,
|
||||
interpreter_name,
|
||||
interpreter_version,
|
||||
mac_platforms,
|
||||
)
|
||||
|
||||
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
|
||||
|
||||
if MYPY_CHECK_RUNNING:
|
||||
from typing import (
|
||||
Tuple, Callable, List, Optional, Union, Dict, Set
|
||||
)
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
Pep425Tag = Tuple[str, str, str]
|
||||
from pipenv.patched.notpip._vendor.packaging.tags import PythonVersion
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_osx_arch_pat = re.compile(r'(.+)_(\d+)_(\d+)_(.+)')
|
||||
|
||||
|
||||
def get_config_var(var):
|
||||
# type: (str) -> Optional[str]
|
||||
try:
|
||||
return sysconfig.get_config_var(var)
|
||||
except IOError as e: # Issue #1074
|
||||
warnings.warn("{}".format(e), RuntimeWarning)
|
||||
return None
|
||||
|
||||
|
||||
def get_abbr_impl():
|
||||
# type: () -> str
|
||||
"""Return abbreviated implementation name."""
|
||||
if hasattr(sys, 'pypy_version_info'):
|
||||
pyimpl = 'pp'
|
||||
elif sys.platform.startswith('java'):
|
||||
pyimpl = 'jy'
|
||||
elif sys.platform == 'cli':
|
||||
pyimpl = 'ip'
|
||||
else:
|
||||
pyimpl = 'cp'
|
||||
return pyimpl
|
||||
|
||||
|
||||
def version_info_to_nodot(version_info):
|
||||
# type: (Tuple[int, ...]) -> str
|
||||
# Only use up to the first two numbers.
|
||||
return ''.join(map(str, version_info[:2]))
|
||||
|
||||
|
||||
def get_impl_ver():
|
||||
# type: () -> str
|
||||
"""Return implementation version."""
|
||||
impl_ver = get_config_var("py_version_nodot")
|
||||
if not impl_ver or get_abbr_impl() == 'pp':
|
||||
impl_ver = ''.join(map(str, get_impl_version_info()))
|
||||
return impl_ver
|
||||
|
||||
|
||||
def get_impl_version_info():
|
||||
# type: () -> Tuple[int, ...]
|
||||
"""Return sys.version_info-like tuple for use in decrementing the minor
|
||||
version."""
|
||||
if get_abbr_impl() == 'pp':
|
||||
# as per https://github.com/pypa/pip/issues/2882
|
||||
# attrs exist only on pypy
|
||||
return (sys.version_info[0],
|
||||
sys.pypy_version_info.major, # type: ignore
|
||||
sys.pypy_version_info.minor) # type: ignore
|
||||
def _mac_platforms(arch):
|
||||
# type: (str) -> List[str]
|
||||
match = _osx_arch_pat.match(arch)
|
||||
if match:
|
||||
name, major, minor, actual_arch = match.groups()
|
||||
mac_version = (int(major), int(minor))
|
||||
arches = [
|
||||
# Since we have always only checked that the platform starts
|
||||
# with "macosx", for backwards-compatibility we extract the
|
||||
# actual prefix provided by the user in case they provided
|
||||
# something like "macosxcustom_". It may be good to remove
|
||||
# this as undocumented or deprecate it in the future.
|
||||
'{}_{}'.format(name, arch[len('macosx_'):])
|
||||
for arch in mac_platforms(mac_version, actual_arch)
|
||||
]
|
||||
else:
|
||||
return sys.version_info[0], sys.version_info[1]
|
||||
|
||||
|
||||
def get_impl_tag():
|
||||
# type: () -> str
|
||||
"""
|
||||
Returns the Tag for this specific implementation.
|
||||
"""
|
||||
return "{}{}".format(get_abbr_impl(), get_impl_ver())
|
||||
|
||||
|
||||
def get_flag(var, fallback, expected=True, warn=True):
|
||||
# type: (str, Callable[..., bool], Union[bool, int], bool) -> bool
|
||||
"""Use a fallback method for determining SOABI flags if the needed config
|
||||
var is unset or unavailable."""
|
||||
val = get_config_var(var)
|
||||
if val is None:
|
||||
if warn:
|
||||
logger.debug("Config variable '%s' is unset, Python ABI tag may "
|
||||
"be incorrect", var)
|
||||
return fallback()
|
||||
return val == expected
|
||||
|
||||
|
||||
def get_abi_tag():
|
||||
# type: () -> Optional[str]
|
||||
"""Return the ABI tag based on SOABI (if available) or emulate SOABI
|
||||
(CPython 2, PyPy)."""
|
||||
soabi = get_config_var('SOABI')
|
||||
impl = get_abbr_impl()
|
||||
abi = None # type: Optional[str]
|
||||
|
||||
if not soabi and impl in {'cp', 'pp'} and hasattr(sys, 'maxunicode'):
|
||||
d = ''
|
||||
m = ''
|
||||
u = ''
|
||||
is_cpython = (impl == 'cp')
|
||||
if get_flag(
|
||||
'Py_DEBUG', lambda: hasattr(sys, 'gettotalrefcount'),
|
||||
warn=is_cpython):
|
||||
d = 'd'
|
||||
if sys.version_info < (3, 8) and get_flag(
|
||||
'WITH_PYMALLOC', lambda: is_cpython, warn=is_cpython):
|
||||
m = 'm'
|
||||
if sys.version_info < (3, 3) and get_flag(
|
||||
'Py_UNICODE_SIZE', lambda: sys.maxunicode == 0x10ffff,
|
||||
expected=4, warn=is_cpython):
|
||||
u = 'u'
|
||||
abi = '%s%s%s%s%s' % (impl, get_impl_ver(), d, m, u)
|
||||
elif soabi and soabi.startswith('cpython-'):
|
||||
abi = 'cp' + soabi.split('-')[1]
|
||||
elif soabi:
|
||||
abi = soabi.replace('.', '_').replace('-', '_')
|
||||
|
||||
return abi
|
||||
|
||||
|
||||
def _is_running_32bit():
|
||||
# type: () -> bool
|
||||
return sys.maxsize == 2147483647
|
||||
|
||||
|
||||
def get_platform():
|
||||
# type: () -> str
|
||||
"""Return our platform name 'win32', 'linux_x86_64'"""
|
||||
if sys.platform == 'darwin':
|
||||
# distutils.util.get_platform() returns the release based on the value
|
||||
# of MACOSX_DEPLOYMENT_TARGET on which Python was built, which may
|
||||
# be significantly older than the user's current machine.
|
||||
release, _, machine = platform.mac_ver()
|
||||
split_ver = release.split('.')
|
||||
|
||||
if machine == "x86_64" and _is_running_32bit():
|
||||
machine = "i386"
|
||||
elif machine == "ppc64" and _is_running_32bit():
|
||||
machine = "ppc"
|
||||
|
||||
return 'macosx_{}_{}_{}'.format(split_ver[0], split_ver[1], machine)
|
||||
|
||||
# XXX remove distutils dependency
|
||||
result = distutils.util.get_platform().replace('.', '_').replace('-', '_')
|
||||
if result == "linux_x86_64" and _is_running_32bit():
|
||||
# 32 bit Python program (running on a 64 bit Linux): pip should only
|
||||
# install and run 32 bit compiled extensions in that case.
|
||||
result = "linux_i686"
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def is_linux_armhf():
|
||||
# type: () -> bool
|
||||
if get_platform() != "linux_armv7l":
|
||||
return False
|
||||
# hard-float ABI can be detected from the ELF header of the running
|
||||
# process
|
||||
sys_executable = os.environ.get('PIP_PYTHON_PATH', sys.executable)
|
||||
try:
|
||||
with open(sys_executable, 'rb') as f:
|
||||
elf_header_raw = f.read(40) # read 40 first bytes of ELF header
|
||||
except (IOError, OSError, TypeError):
|
||||
return False
|
||||
if elf_header_raw is None or len(elf_header_raw) < 40:
|
||||
return False
|
||||
if isinstance(elf_header_raw, str):
|
||||
elf_header = [ord(c) for c in elf_header_raw]
|
||||
else:
|
||||
elf_header = [b for b in elf_header_raw]
|
||||
result = elf_header[0:4] == [0x7f, 0x45, 0x4c, 0x46] # ELF magic number
|
||||
result &= elf_header[4:5] == [1] # 32-bit ELF
|
||||
result &= elf_header[5:6] == [1] # little-endian
|
||||
result &= elf_header[18:20] == [0x28, 0] # ARM machine
|
||||
result &= elf_header[39:40] == [5] # ARM EABIv5
|
||||
result &= (elf_header[37:38][0] & 4) == 4 # EF_ARM_ABI_FLOAT_HARD
|
||||
return result
|
||||
|
||||
|
||||
def is_manylinux1_compatible():
|
||||
# type: () -> bool
|
||||
# Only Linux, and only x86-64 / i686
|
||||
if get_platform() not in {"linux_x86_64", "linux_i686"}:
|
||||
return False
|
||||
|
||||
# Check for presence of _manylinux module
|
||||
try:
|
||||
import _manylinux
|
||||
return bool(_manylinux.manylinux1_compatible)
|
||||
except (ImportError, AttributeError):
|
||||
# Fall through to heuristic check below
|
||||
pass
|
||||
|
||||
# Check glibc version. CentOS 5 uses glibc 2.5.
|
||||
return pipenv.patched.notpip._internal.utils.glibc.have_compatible_glibc(2, 5)
|
||||
|
||||
|
||||
def is_manylinux2010_compatible():
|
||||
# type: () -> bool
|
||||
# Only Linux, and only x86-64 / i686
|
||||
if get_platform() not in {"linux_x86_64", "linux_i686"}:
|
||||
return False
|
||||
|
||||
# Check for presence of _manylinux module
|
||||
try:
|
||||
import _manylinux
|
||||
return bool(_manylinux.manylinux2010_compatible)
|
||||
except (ImportError, AttributeError):
|
||||
# Fall through to heuristic check below
|
||||
pass
|
||||
|
||||
# Check glibc version. CentOS 6 uses glibc 2.12.
|
||||
return pipenv.patched.notpip._internal.utils.glibc.have_compatible_glibc(2, 12)
|
||||
|
||||
|
||||
def is_manylinux2014_compatible():
|
||||
# type: () -> bool
|
||||
# Only Linux, and only supported architectures
|
||||
platform = get_platform()
|
||||
if platform not in {"linux_x86_64", "linux_i686", "linux_aarch64",
|
||||
"linux_armv7l", "linux_ppc64", "linux_ppc64le",
|
||||
"linux_s390x"}:
|
||||
return False
|
||||
|
||||
# check for hard-float ABI in case we're running linux_armv7l not to
|
||||
# install hard-float ABI wheel in a soft-float ABI environment
|
||||
if platform == "linux_armv7l" and not is_linux_armhf():
|
||||
return False
|
||||
|
||||
# Check for presence of _manylinux module
|
||||
try:
|
||||
import _manylinux
|
||||
return bool(_manylinux.manylinux2014_compatible)
|
||||
except (ImportError, AttributeError):
|
||||
# Fall through to heuristic check below
|
||||
pass
|
||||
|
||||
# Check glibc version. CentOS 7 uses glibc 2.17.
|
||||
return pipenv.patched.notpip._internal.utils.glibc.have_compatible_glibc(2, 17)
|
||||
|
||||
|
||||
def get_darwin_arches(major, minor, machine):
|
||||
# type: (int, int, str) -> List[str]
|
||||
"""Return a list of supported arches (including group arches) for
|
||||
the given major, minor and machine architecture of an macOS machine.
|
||||
"""
|
||||
arches = []
|
||||
|
||||
def _supports_arch(major, minor, arch):
|
||||
# type: (int, int, str) -> bool
|
||||
# Looking at the application support for macOS versions in the chart
|
||||
# provided by https://en.wikipedia.org/wiki/OS_X#Versions it appears
|
||||
# our timeline looks roughly like:
|
||||
#
|
||||
# 10.0 - Introduces ppc support.
|
||||
# 10.4 - Introduces ppc64, i386, and x86_64 support, however the ppc64
|
||||
# and x86_64 support is CLI only, and cannot be used for GUI
|
||||
# applications.
|
||||
# 10.5 - Extends ppc64 and x86_64 support to cover GUI applications.
|
||||
# 10.6 - Drops support for ppc64
|
||||
# 10.7 - Drops support for ppc
|
||||
#
|
||||
# Given that we do not know if we're installing a CLI or a GUI
|
||||
# application, we must be conservative and assume it might be a GUI
|
||||
# application and behave as if ppc64 and x86_64 support did not occur
|
||||
# until 10.5.
|
||||
#
|
||||
# Note: The above information is taken from the "Application support"
|
||||
# column in the chart not the "Processor support" since I believe
|
||||
# that we care about what instruction sets an application can use
|
||||
# not which processors the OS supports.
|
||||
if arch == 'ppc':
|
||||
return (major, minor) <= (10, 5)
|
||||
if arch == 'ppc64':
|
||||
return (major, minor) == (10, 5)
|
||||
if arch == 'i386':
|
||||
return (major, minor) >= (10, 4)
|
||||
if arch == 'x86_64':
|
||||
return (major, minor) >= (10, 5)
|
||||
if arch in groups:
|
||||
for garch in groups[arch]:
|
||||
if _supports_arch(major, minor, garch):
|
||||
return True
|
||||
return False
|
||||
|
||||
groups = OrderedDict([
|
||||
("fat", ("i386", "ppc")),
|
||||
("intel", ("x86_64", "i386")),
|
||||
("fat64", ("x86_64", "ppc64")),
|
||||
("fat32", ("x86_64", "i386", "ppc")),
|
||||
]) # type: Dict[str, Tuple[str, ...]]
|
||||
|
||||
if _supports_arch(major, minor, machine):
|
||||
arches.append(machine)
|
||||
|
||||
for garch in groups:
|
||||
if machine in groups[garch] and _supports_arch(major, minor, garch):
|
||||
arches.append(garch)
|
||||
|
||||
arches.append('universal')
|
||||
|
||||
# arch pattern didn't match (?!)
|
||||
arches = [arch]
|
||||
return arches
|
||||
|
||||
|
||||
def get_all_minor_versions_as_strings(version_info):
|
||||
# type: (Tuple[int, ...]) -> List[str]
|
||||
versions = []
|
||||
major = version_info[:-1]
|
||||
# Support all previous minor Python versions.
|
||||
for minor in range(version_info[-1], -1, -1):
|
||||
versions.append(''.join(map(str, major + (minor,))))
|
||||
return versions
|
||||
def _custom_manylinux_platforms(arch):
|
||||
# type: (str) -> List[str]
|
||||
arches = [arch]
|
||||
arch_prefix, arch_sep, arch_suffix = arch.partition('_')
|
||||
if arch_prefix == 'manylinux2014':
|
||||
# manylinux1/manylinux2010 wheels run on most manylinux2014 systems
|
||||
# with the exception of wheels depending on ncurses. PEP 599 states
|
||||
# manylinux1/manylinux2010 wheels should be considered
|
||||
# manylinux2014 wheels:
|
||||
# https://www.python.org/dev/peps/pep-0599/#backwards-compatibility-with-manylinux2010-wheels
|
||||
if arch_suffix in {'i686', 'x86_64'}:
|
||||
arches.append('manylinux2010' + arch_sep + arch_suffix)
|
||||
arches.append('manylinux1' + arch_sep + arch_suffix)
|
||||
elif arch_prefix == 'manylinux2010':
|
||||
# manylinux1 wheels run on most manylinux2010 systems with the
|
||||
# exception of wheels depending on ncurses. PEP 571 states
|
||||
# manylinux1 wheels should be considered manylinux2010 wheels:
|
||||
# https://www.python.org/dev/peps/pep-0571/#backwards-compatibility-with-manylinux1-wheels
|
||||
arches.append('manylinux1' + arch_sep + arch_suffix)
|
||||
return arches
|
||||
|
||||
|
||||
def _get_custom_platforms(arch):
|
||||
# type: (str) -> List[str]
|
||||
arch_prefix, arch_sep, arch_suffix = arch.partition('_')
|
||||
if arch.startswith('macosx'):
|
||||
arches = _mac_platforms(arch)
|
||||
elif arch_prefix in ['manylinux2014', 'manylinux2010']:
|
||||
arches = _custom_manylinux_platforms(arch)
|
||||
else:
|
||||
arches = [arch]
|
||||
return arches
|
||||
|
||||
|
||||
def _get_python_version(version):
|
||||
# type: (str) -> PythonVersion
|
||||
if len(version) > 1:
|
||||
return int(version[0]), int(version[1:])
|
||||
else:
|
||||
return (int(version[0]),)
|
||||
|
||||
|
||||
def _get_custom_interpreter(implementation=None, version=None):
|
||||
# type: (Optional[str], Optional[str]) -> str
|
||||
if implementation is None:
|
||||
implementation = interpreter_name()
|
||||
if version is None:
|
||||
version = interpreter_version()
|
||||
return "{}{}".format(implementation, version)
|
||||
|
||||
|
||||
def get_supported(
|
||||
versions=None, # type: Optional[List[str]]
|
||||
noarch=False, # type: bool
|
||||
version=None, # type: Optional[str]
|
||||
platform=None, # type: Optional[str]
|
||||
impl=None, # type: Optional[str]
|
||||
abi=None # type: Optional[str]
|
||||
):
|
||||
# type: (...) -> List[Pep425Tag]
|
||||
# type: (...) -> List[Tag]
|
||||
"""Return a list of supported tags for each version specified in
|
||||
`versions`.
|
||||
|
||||
:param versions: a list of string versions, of the form ["33", "32"],
|
||||
or None. The first version will be assumed to support our ABI.
|
||||
:param version: a string version, of the form "33" or "32",
|
||||
or None. The version will be assumed to support our ABI.
|
||||
:param platform: specify the exact platform you want valid
|
||||
tags for, or None. If None, use the local system platform.
|
||||
:param impl: specify the exact implementation you want valid
|
||||
@@ -347,105 +123,45 @@ def get_supported(
|
||||
:param abi: specify the exact abi you want valid
|
||||
tags for, or None. If None, use the local interpreter abi.
|
||||
"""
|
||||
supported = []
|
||||
supported = [] # type: List[Tag]
|
||||
|
||||
# Versions must be given with respect to the preference
|
||||
if versions is None:
|
||||
version_info = get_impl_version_info()
|
||||
versions = get_all_minor_versions_as_strings(version_info)
|
||||
python_version = None # type: Optional[PythonVersion]
|
||||
if version is not None:
|
||||
python_version = _get_python_version(version)
|
||||
|
||||
impl = impl or get_abbr_impl()
|
||||
interpreter = _get_custom_interpreter(impl, version)
|
||||
|
||||
abis = [] # type: List[str]
|
||||
abis = None # type: Optional[List[str]]
|
||||
if abi is not None:
|
||||
abis = [abi]
|
||||
|
||||
abi = abi or get_abi_tag()
|
||||
if abi:
|
||||
abis[0:0] = [abi]
|
||||
platforms = None # type: Optional[List[str]]
|
||||
if platform is not None:
|
||||
platforms = _get_custom_platforms(platform)
|
||||
|
||||
abi3s = set() # type: Set[str]
|
||||
for suffix in get_extension_suffixes():
|
||||
if suffix.startswith('.abi'):
|
||||
abi3s.add(suffix.split('.', 2)[1])
|
||||
|
||||
abis.extend(sorted(list(abi3s)))
|
||||
|
||||
abis.append('none')
|
||||
|
||||
if not noarch:
|
||||
arch = platform or get_platform()
|
||||
arch_prefix, arch_sep, arch_suffix = arch.partition('_')
|
||||
if arch.startswith('macosx'):
|
||||
# support macosx-10.6-intel on macosx-10.9-x86_64
|
||||
match = _osx_arch_pat.match(arch)
|
||||
if match:
|
||||
name, major, minor, actual_arch = match.groups()
|
||||
tpl = '{}_{}_%i_%s'.format(name, major)
|
||||
arches = []
|
||||
for m in reversed(range(int(minor) + 1)):
|
||||
for a in get_darwin_arches(int(major), m, actual_arch):
|
||||
arches.append(tpl % (m, a))
|
||||
else:
|
||||
# arch pattern didn't match (?!)
|
||||
arches = [arch]
|
||||
elif arch_prefix == 'manylinux2014':
|
||||
arches = [arch]
|
||||
# manylinux1/manylinux2010 wheels run on most manylinux2014 systems
|
||||
# with the exception of wheels depending on ncurses. PEP 599 states
|
||||
# manylinux1/manylinux2010 wheels should be considered
|
||||
# manylinux2014 wheels:
|
||||
# https://www.python.org/dev/peps/pep-0599/#backwards-compatibility-with-manylinux2010-wheels
|
||||
if arch_suffix in {'i686', 'x86_64'}:
|
||||
arches.append('manylinux2010' + arch_sep + arch_suffix)
|
||||
arches.append('manylinux1' + arch_sep + arch_suffix)
|
||||
elif arch_prefix == 'manylinux2010':
|
||||
# manylinux1 wheels run on most manylinux2010 systems with the
|
||||
# exception of wheels depending on ncurses. PEP 571 states
|
||||
# manylinux1 wheels should be considered manylinux2010 wheels:
|
||||
# https://www.python.org/dev/peps/pep-0571/#backwards-compatibility-with-manylinux1-wheels
|
||||
arches = [arch, 'manylinux1' + arch_sep + arch_suffix]
|
||||
elif platform is None:
|
||||
arches = []
|
||||
if is_manylinux2014_compatible():
|
||||
arches.append('manylinux2014' + arch_sep + arch_suffix)
|
||||
if is_manylinux2010_compatible():
|
||||
arches.append('manylinux2010' + arch_sep + arch_suffix)
|
||||
if is_manylinux1_compatible():
|
||||
arches.append('manylinux1' + arch_sep + arch_suffix)
|
||||
arches.append(arch)
|
||||
else:
|
||||
arches = [arch]
|
||||
|
||||
# Current version, current API (built specifically for our Python):
|
||||
for abi in abis:
|
||||
for arch in arches:
|
||||
supported.append(('%s%s' % (impl, versions[0]), abi, arch))
|
||||
|
||||
# abi3 modules compatible with older version of Python
|
||||
for version in versions[1:]:
|
||||
# abi3 was introduced in Python 3.2
|
||||
if version in {'31', '30'}:
|
||||
break
|
||||
for abi in abi3s: # empty set if not Python 3
|
||||
for arch in arches:
|
||||
supported.append(("%s%s" % (impl, version), abi, arch))
|
||||
|
||||
# Has binaries, does not use the Python API:
|
||||
for arch in arches:
|
||||
supported.append(('py%s' % (versions[0][0]), 'none', arch))
|
||||
|
||||
# No abi / arch, but requires our implementation:
|
||||
supported.append(('%s%s' % (impl, versions[0]), 'none', 'any'))
|
||||
# Tagged specifically as being cross-version compatible
|
||||
# (with just the major version specified)
|
||||
supported.append(('%s%s' % (impl, versions[0][0]), 'none', 'any'))
|
||||
|
||||
# No abi / arch, generic Python
|
||||
for i, version in enumerate(versions):
|
||||
supported.append(('py%s' % (version,), 'none', 'any'))
|
||||
if i == 0:
|
||||
supported.append(('py%s' % (version[0]), 'none', 'any'))
|
||||
is_cpython = (impl or interpreter_name()) == "cp"
|
||||
if is_cpython:
|
||||
supported.extend(
|
||||
cpython_tags(
|
||||
python_version=python_version,
|
||||
abis=abis,
|
||||
platforms=platforms,
|
||||
)
|
||||
)
|
||||
else:
|
||||
supported.extend(
|
||||
generic_tags(
|
||||
interpreter=interpreter,
|
||||
abis=abis,
|
||||
platforms=platforms,
|
||||
)
|
||||
)
|
||||
supported.extend(
|
||||
compatible_tags(
|
||||
python_version=python_version,
|
||||
interpreter=interpreter,
|
||||
platforms=platforms,
|
||||
)
|
||||
)
|
||||
|
||||
return supported
|
||||
|
||||
|
||||
implementation_tag = get_impl_tag()
|
||||
|
||||
@@ -3,14 +3,16 @@ from __future__ import absolute_import
|
||||
import io
|
||||
import os
|
||||
import sys
|
||||
from collections import namedtuple
|
||||
|
||||
from pipenv.patched.notpip._vendor import pytoml, six
|
||||
from pipenv.patched.notpip._vendor.packaging.requirements import InvalidRequirement, Requirement
|
||||
|
||||
from pipenv.patched.notpip._internal.exceptions import InstallationError
|
||||
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
|
||||
|
||||
if MYPY_CHECK_RUNNING:
|
||||
from typing import Any, Tuple, Optional, List
|
||||
from typing import Any, Optional, List
|
||||
|
||||
|
||||
def _is_list_of_str(obj):
|
||||
@@ -32,13 +34,18 @@ def make_pyproject_path(unpacked_source_directory):
|
||||
return 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[Tuple[List[str], str, List[str]]]
|
||||
# type: (...) -> Optional[BuildSystemDetails]
|
||||
"""Load the pyproject.toml file.
|
||||
|
||||
Parameters:
|
||||
@@ -56,6 +63,8 @@ def load_pyproject_toml(
|
||||
name of PEP 517 backend,
|
||||
requirements we should check are installed after setting
|
||||
up the build environment
|
||||
directory paths to import the backend from (backend-path),
|
||||
relative to the project root.
|
||||
)
|
||||
"""
|
||||
has_pyproject = os.path.isfile(pyproject_toml)
|
||||
@@ -150,7 +159,23 @@ def load_pyproject_toml(
|
||||
reason="'build-system.requires' 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)
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
backend = build_system.get("build-backend")
|
||||
backend_path = build_system.get("backend-path", [])
|
||||
check = [] # type: List[str]
|
||||
if backend is None:
|
||||
# If the user didn't specify a backend, we assume they want to use
|
||||
@@ -168,4 +193,4 @@ def load_pyproject_toml(
|
||||
backend = "setuptools.build_meta:__legacy__"
|
||||
check = ["setuptools>=40.8.0", "wheel"]
|
||||
|
||||
return (requires, backend, check)
|
||||
return BuildSystemDetails(requires, backend, check, backend_path)
|
||||
|
||||
@@ -23,6 +23,16 @@ __all__ = [
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class InstallationResult(object):
|
||||
def __init__(self, name):
|
||||
# type: (str) -> None
|
||||
self.name = name
|
||||
|
||||
def __repr__(self):
|
||||
# type: () -> str
|
||||
return "InstallationResult(name={!r})".format(self.name)
|
||||
|
||||
|
||||
def install_given_reqs(
|
||||
to_install, # type: List[InstallRequirement]
|
||||
install_options, # type: List[str]
|
||||
@@ -30,7 +40,7 @@ def install_given_reqs(
|
||||
*args, # type: Any
|
||||
**kwargs # type: Any
|
||||
):
|
||||
# type: (...) -> List[InstallRequirement]
|
||||
# type: (...) -> List[InstallationResult]
|
||||
"""
|
||||
Install everything in the given list.
|
||||
|
||||
@@ -43,13 +53,12 @@ def install_given_reqs(
|
||||
', '.join([req.name for req in to_install]),
|
||||
)
|
||||
|
||||
installed = []
|
||||
|
||||
with indent_log():
|
||||
for requirement in to_install:
|
||||
if requirement.conflicts_with:
|
||||
logger.info(
|
||||
'Found existing installation: %s',
|
||||
requirement.conflicts_with,
|
||||
)
|
||||
if requirement.should_reinstall:
|
||||
logger.info('Attempting uninstall: %s', requirement.name)
|
||||
with indent_log():
|
||||
uninstalled_pathset = requirement.uninstall(
|
||||
auto_confirm=True
|
||||
@@ -63,7 +72,7 @@ def install_given_reqs(
|
||||
)
|
||||
except Exception:
|
||||
should_rollback = (
|
||||
requirement.conflicts_with and
|
||||
requirement.should_reinstall and
|
||||
not requirement.install_succeeded
|
||||
)
|
||||
# if install did not succeed, rollback previous uninstall
|
||||
@@ -72,11 +81,12 @@ def install_given_reqs(
|
||||
raise
|
||||
else:
|
||||
should_commit = (
|
||||
requirement.conflicts_with and
|
||||
requirement.should_reinstall and
|
||||
requirement.install_succeeded
|
||||
)
|
||||
if should_commit:
|
||||
uninstalled_pathset.commit()
|
||||
requirement.remove_temporary_source()
|
||||
|
||||
return to_install
|
||||
installed.append(InstallationResult(requirement.name))
|
||||
|
||||
return installed
|
||||
|
||||
@@ -10,7 +10,6 @@ InstallRequirement.
|
||||
|
||||
# The following comment should be removed at some point in the future.
|
||||
# mypy: strict-optional=False
|
||||
# mypy: disallow-untyped-defs=False
|
||||
|
||||
import logging
|
||||
import os
|
||||
@@ -24,6 +23,7 @@ from pipenv.patched.notpip._vendor.pkg_resources import RequirementParseError, p
|
||||
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_install import InstallRequirement
|
||||
from pipenv.patched.notpip._internal.utils.filetypes import ARCHIVE_EXTENSIONS
|
||||
@@ -31,7 +31,6 @@ from pipenv.patched.notpip._internal.utils.misc import is_installable_dir, split
|
||||
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
|
||||
from pipenv.patched.notpip._internal.utils.urls import path_to_url
|
||||
from pipenv.patched.notpip._internal.vcs import is_url, vcs
|
||||
from pipenv.patched.notpip._internal.wheel import Wheel
|
||||
|
||||
if MYPY_CHECK_RUNNING:
|
||||
from typing import (
|
||||
@@ -347,6 +346,7 @@ def parse_req_from_line(name, line_source):
|
||||
extras = convert_extras(extras_as_string)
|
||||
|
||||
def with_source(text):
|
||||
# type: (str) -> str
|
||||
if not line_source:
|
||||
return text
|
||||
return '{} (from {})'.format(text, line_source)
|
||||
|
||||
@@ -17,26 +17,35 @@ from pipenv.patched.notpip._vendor.six.moves import filterfalse
|
||||
from pipenv.patched.notpip._vendor.six.moves.urllib import parse as urllib_parse
|
||||
|
||||
from pipenv.patched.notpip._internal.cli import cmdoptions
|
||||
from pipenv.patched.notpip._internal.download import get_file_content
|
||||
from pipenv.patched.notpip._internal.exceptions import RequirementsFileParseError
|
||||
from pipenv.patched.notpip._internal.exceptions import (
|
||||
InstallationError,
|
||||
RequirementsFileParseError,
|
||||
)
|
||||
from pipenv.patched.notpip._internal.models.search_scope import SearchScope
|
||||
from pipenv.patched.notpip._internal.req.constructors import (
|
||||
install_req_from_editable,
|
||||
install_req_from_line,
|
||||
)
|
||||
from pipenv.patched.notpip._internal.utils.encoding import auto_decode
|
||||
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
|
||||
from pipenv.patched.notpip._internal.utils.urls import get_url_scheme
|
||||
|
||||
if MYPY_CHECK_RUNNING:
|
||||
from optparse import Values
|
||||
from typing import (
|
||||
Any, Callable, Iterator, List, NoReturn, Optional, Text, Tuple,
|
||||
)
|
||||
|
||||
from pipenv.patched.notpip._internal.req import InstallRequirement
|
||||
from pipenv.patched.notpip._internal.cache import WheelCache
|
||||
from pipenv.patched.notpip._internal.index import PackageFinder
|
||||
from pipenv.patched.notpip._internal.index.package_finder import PackageFinder
|
||||
from pipenv.patched.notpip._internal.network.session import PipSession
|
||||
|
||||
ReqFileLines = Iterator[Tuple[int, Text]]
|
||||
|
||||
LineParser = Callable[[Text], Tuple[str, Values]]
|
||||
|
||||
|
||||
__all__ = ['parse_requirements']
|
||||
|
||||
SCHEME_RE = re.compile(r'^(http|https|file):', re.I)
|
||||
@@ -49,19 +58,19 @@ COMMENT_RE = re.compile(r'(^|\s+)#.*$')
|
||||
ENV_VAR_RE = re.compile(r'(?P<var>\$\{(?P<name>[A-Z0-9_]+)\})')
|
||||
|
||||
SUPPORTED_OPTIONS = [
|
||||
cmdoptions.constraints,
|
||||
cmdoptions.editable,
|
||||
cmdoptions.requirements,
|
||||
cmdoptions.no_index,
|
||||
cmdoptions.index_url,
|
||||
cmdoptions.find_links,
|
||||
cmdoptions.extra_index_url,
|
||||
cmdoptions.always_unzip,
|
||||
cmdoptions.no_index,
|
||||
cmdoptions.constraints,
|
||||
cmdoptions.requirements,
|
||||
cmdoptions.editable,
|
||||
cmdoptions.find_links,
|
||||
cmdoptions.no_binary,
|
||||
cmdoptions.only_binary,
|
||||
cmdoptions.require_hashes,
|
||||
cmdoptions.pre,
|
||||
cmdoptions.trusted_host,
|
||||
cmdoptions.require_hashes,
|
||||
cmdoptions.always_unzip, # Deprecated
|
||||
] # type: List[Callable[..., optparse.Option]]
|
||||
|
||||
# options to be passed to requirements
|
||||
@@ -75,12 +84,31 @@ SUPPORTED_OPTIONS_REQ = [
|
||||
SUPPORTED_OPTIONS_REQ_DEST = [str(o().dest) for o in SUPPORTED_OPTIONS_REQ]
|
||||
|
||||
|
||||
class ParsedLine(object):
|
||||
def __init__(
|
||||
self,
|
||||
filename, # type: str
|
||||
lineno, # type: int
|
||||
comes_from, # type: str
|
||||
args, # type: str
|
||||
opts, # type: Values
|
||||
constraint, # type: bool
|
||||
):
|
||||
# type: (...) -> None
|
||||
self.filename = filename
|
||||
self.lineno = lineno
|
||||
self.comes_from = comes_from
|
||||
self.args = args
|
||||
self.opts = opts
|
||||
self.constraint = constraint
|
||||
|
||||
|
||||
def parse_requirements(
|
||||
filename, # type: str
|
||||
session, # type: PipSession
|
||||
finder=None, # type: Optional[PackageFinder]
|
||||
comes_from=None, # type: Optional[str]
|
||||
options=None, # type: Optional[optparse.Values]
|
||||
session=None, # type: Optional[PipSession]
|
||||
constraint=False, # type: bool
|
||||
wheel_cache=None, # type: Optional[WheelCache]
|
||||
use_pep517=None # type: Optional[bool]
|
||||
@@ -89,37 +117,33 @@ def parse_requirements(
|
||||
"""Parse a requirements file and yield InstallRequirement instances.
|
||||
|
||||
:param filename: Path or url of requirements file.
|
||||
:param session: PipSession instance.
|
||||
:param finder: Instance of pip.index.PackageFinder.
|
||||
:param comes_from: Origin description of requirements.
|
||||
:param options: cli options.
|
||||
:param session: Instance of pip.download.PipSession.
|
||||
:param constraint: If true, parsing a constraint file rather than
|
||||
requirements file.
|
||||
:param wheel_cache: Instance of pip.wheel.WheelCache
|
||||
:param use_pep517: Value of the --use-pep517 option.
|
||||
"""
|
||||
if session is None:
|
||||
raise TypeError(
|
||||
"parse_requirements() missing 1 required keyword argument: "
|
||||
"'session'"
|
||||
)
|
||||
|
||||
_, content = get_file_content(
|
||||
filename, comes_from=comes_from, session=session
|
||||
skip_requirements_regex = (
|
||||
options.skip_requirements_regex if options else None
|
||||
)
|
||||
line_parser = get_line_parser(finder)
|
||||
parser = RequirementsFileParser(
|
||||
session, line_parser, comes_from, skip_requirements_regex
|
||||
)
|
||||
|
||||
lines_enum = preprocess(content, options)
|
||||
|
||||
for line_number, line in lines_enum:
|
||||
req_iter = process_line(line, filename, line_number, finder,
|
||||
comes_from, options, session, wheel_cache,
|
||||
use_pep517=use_pep517, constraint=constraint)
|
||||
for req in req_iter:
|
||||
for parsed_line in parser.parse(filename, constraint):
|
||||
req = handle_line(
|
||||
parsed_line, finder, options, session, wheel_cache, use_pep517
|
||||
)
|
||||
if req is not None:
|
||||
yield req
|
||||
|
||||
|
||||
def preprocess(content, options):
|
||||
# type: (Text, Optional[optparse.Values]) -> ReqFileLines
|
||||
def preprocess(content, skip_requirements_regex):
|
||||
# type: (Text, Optional[str]) -> ReqFileLines
|
||||
"""Split, filter, and join lines, and return a line iterator
|
||||
|
||||
:param content: the content of the requirements file
|
||||
@@ -128,26 +152,23 @@ def preprocess(content, options):
|
||||
lines_enum = enumerate(content.splitlines(), start=1) # type: ReqFileLines
|
||||
lines_enum = join_lines(lines_enum)
|
||||
lines_enum = ignore_comments(lines_enum)
|
||||
lines_enum = skip_regex(lines_enum, options)
|
||||
if skip_requirements_regex:
|
||||
lines_enum = skip_regex(lines_enum, skip_requirements_regex)
|
||||
lines_enum = expand_env_variables(lines_enum)
|
||||
return lines_enum
|
||||
|
||||
|
||||
def process_line(
|
||||
line, # type: Text
|
||||
filename, # type: str
|
||||
line_number, # type: int
|
||||
def handle_line(
|
||||
line, # type: ParsedLine
|
||||
finder=None, # type: Optional[PackageFinder]
|
||||
comes_from=None, # type: Optional[str]
|
||||
options=None, # type: Optional[optparse.Values]
|
||||
session=None, # type: Optional[PipSession]
|
||||
wheel_cache=None, # type: Optional[WheelCache]
|
||||
use_pep517=None, # type: Optional[bool]
|
||||
constraint=False, # type: bool
|
||||
):
|
||||
# type: (...) -> Iterator[InstallRequirement]
|
||||
"""Process a single requirements line; This can result in creating/yielding
|
||||
requirements, or updating the finder.
|
||||
# type: (...) -> Optional[InstallRequirement]
|
||||
"""Handle a single parsed requirements line; This can result in
|
||||
creating/yielding requirements, or updating the finder.
|
||||
|
||||
For lines that contain requirements, the only options that have an effect
|
||||
are from SUPPORTED_OPTIONS_REQ, and they are scoped to the
|
||||
@@ -159,104 +180,65 @@ def process_line(
|
||||
be present, but are ignored. These lines may contain multiple options
|
||||
(although our docs imply only one is supported), and all our parsed and
|
||||
affect the finder.
|
||||
|
||||
:param constraint: If True, parsing a constraints file.
|
||||
:param options: OptionParser options that we may update
|
||||
"""
|
||||
parser = build_parser(line)
|
||||
defaults = parser.get_default_values()
|
||||
defaults.index_url = None
|
||||
if finder:
|
||||
defaults.format_control = finder.format_control
|
||||
args_str, options_str = break_args_options(line)
|
||||
# Prior to 2.7.3, shlex cannot deal with unicode entries
|
||||
if sys.version_info < (2, 7, 3):
|
||||
# https://github.com/python/mypy/issues/1174
|
||||
options_str = options_str.encode('utf8') # type: ignore
|
||||
# https://github.com/python/mypy/issues/1174
|
||||
opts, _ = parser.parse_args(
|
||||
shlex.split(options_str), defaults) # type: ignore
|
||||
|
||||
# preserve for the nested code path
|
||||
line_comes_from = '%s %s (line %s)' % (
|
||||
'-c' if constraint else '-r', filename, line_number,
|
||||
'-c' if line.constraint else '-r', line.filename, line.lineno,
|
||||
)
|
||||
|
||||
# yield a line requirement
|
||||
if args_str:
|
||||
# return a line requirement
|
||||
if line.args:
|
||||
isolated = options.isolated_mode if options else False
|
||||
if options:
|
||||
cmdoptions.check_install_build_global(options, opts)
|
||||
cmdoptions.check_install_build_global(options, line.opts)
|
||||
# get the options that apply to requirements
|
||||
req_options = {}
|
||||
for dest in SUPPORTED_OPTIONS_REQ_DEST:
|
||||
if dest in opts.__dict__ and opts.__dict__[dest]:
|
||||
req_options[dest] = opts.__dict__[dest]
|
||||
line_source = 'line {} of {}'.format(line_number, filename)
|
||||
yield install_req_from_line(
|
||||
args_str,
|
||||
if dest in line.opts.__dict__ and line.opts.__dict__[dest]:
|
||||
req_options[dest] = line.opts.__dict__[dest]
|
||||
line_source = 'line {} of {}'.format(line.lineno, line.filename)
|
||||
return install_req_from_line(
|
||||
line.args,
|
||||
comes_from=line_comes_from,
|
||||
use_pep517=use_pep517,
|
||||
isolated=isolated,
|
||||
options=req_options,
|
||||
wheel_cache=wheel_cache,
|
||||
constraint=constraint,
|
||||
constraint=line.constraint,
|
||||
line_source=line_source,
|
||||
)
|
||||
|
||||
# yield an editable requirement
|
||||
elif opts.editables:
|
||||
# return an editable requirement
|
||||
elif line.opts.editables:
|
||||
isolated = options.isolated_mode if options else False
|
||||
yield install_req_from_editable(
|
||||
opts.editables[0], comes_from=line_comes_from,
|
||||
return install_req_from_editable(
|
||||
line.opts.editables[0], comes_from=line_comes_from,
|
||||
use_pep517=use_pep517,
|
||||
constraint=constraint, isolated=isolated, wheel_cache=wheel_cache
|
||||
constraint=line.constraint, isolated=isolated,
|
||||
wheel_cache=wheel_cache
|
||||
)
|
||||
|
||||
# parse a nested requirements file
|
||||
elif opts.requirements or opts.constraints:
|
||||
if opts.requirements:
|
||||
req_path = opts.requirements[0]
|
||||
nested_constraint = False
|
||||
else:
|
||||
req_path = opts.constraints[0]
|
||||
nested_constraint = True
|
||||
# original file is over http
|
||||
if SCHEME_RE.search(filename):
|
||||
# do a url join so relative paths work
|
||||
req_path = urllib_parse.urljoin(filename, req_path)
|
||||
# original file and nested file are paths
|
||||
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)
|
||||
# TODO: Why not use `comes_from='-r {} (line {})'` here as well?
|
||||
parsed_reqs = parse_requirements(
|
||||
req_path, finder, comes_from, options, session,
|
||||
constraint=nested_constraint, wheel_cache=wheel_cache
|
||||
)
|
||||
for req in parsed_reqs:
|
||||
yield req
|
||||
|
||||
# percolate hash-checking option upward
|
||||
elif opts.require_hashes:
|
||||
options.require_hashes = opts.require_hashes
|
||||
elif line.opts.require_hashes:
|
||||
options.require_hashes = line.opts.require_hashes
|
||||
|
||||
# set finder options
|
||||
elif finder:
|
||||
find_links = finder.find_links
|
||||
index_urls = finder.index_urls
|
||||
if opts.index_url:
|
||||
index_urls = [opts.index_url]
|
||||
if opts.no_index is True:
|
||||
if line.opts.index_url:
|
||||
index_urls = [line.opts.index_url]
|
||||
if line.opts.no_index is True:
|
||||
index_urls = []
|
||||
if opts.extra_index_urls:
|
||||
index_urls.extend(opts.extra_index_urls)
|
||||
if opts.find_links:
|
||||
if line.opts.extra_index_urls:
|
||||
index_urls.extend(line.opts.extra_index_urls)
|
||||
if line.opts.find_links:
|
||||
# FIXME: it would be nice to keep track of the source
|
||||
# of the find_links: support a find-links local path
|
||||
# relative to a requirements file.
|
||||
value = opts.find_links[0]
|
||||
req_dir = os.path.dirname(os.path.abspath(filename))
|
||||
value = line.opts.find_links[0]
|
||||
req_dir = os.path.dirname(os.path.abspath(line.filename))
|
||||
relative_to_reqs_file = os.path.join(req_dir, value)
|
||||
if os.path.exists(relative_to_reqs_file):
|
||||
value = relative_to_reqs_file
|
||||
@@ -268,11 +250,123 @@ def process_line(
|
||||
)
|
||||
finder.search_scope = search_scope
|
||||
|
||||
if opts.pre:
|
||||
if line.opts.pre:
|
||||
finder.set_allow_all_prereleases()
|
||||
for host in opts.trusted_hosts or []:
|
||||
source = 'line {} of {}'.format(line_number, filename)
|
||||
session.add_trusted_host(host, source=source)
|
||||
|
||||
if session:
|
||||
for host in line.opts.trusted_hosts or []:
|
||||
source = 'line {} of {}'.format(line.lineno, line.filename)
|
||||
session.add_trusted_host(host, source=source)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class RequirementsFileParser(object):
|
||||
def __init__(
|
||||
self,
|
||||
session, # type: PipSession
|
||||
line_parser, # type: LineParser
|
||||
comes_from, # type: str
|
||||
skip_requirements_regex, # type: Optional[str]
|
||||
):
|
||||
# type: (...) -> None
|
||||
self._session = session
|
||||
self._line_parser = line_parser
|
||||
self._comes_from = comes_from
|
||||
self._skip_requirements_regex = skip_requirements_regex
|
||||
|
||||
def parse(self, filename, constraint):
|
||||
# type: (str, bool) -> Iterator[ParsedLine]
|
||||
"""Parse a given file, yielding parsed lines.
|
||||
"""
|
||||
for line in self._parse_and_recurse(filename, constraint):
|
||||
yield line
|
||||
|
||||
def _parse_and_recurse(self, filename, constraint):
|
||||
# type: (str, bool) -> Iterator[ParsedLine]
|
||||
for line in self._parse_file(filename, constraint):
|
||||
if (
|
||||
not line.args and
|
||||
not line.opts.editables and
|
||||
(line.opts.requirements or line.opts.constraints)
|
||||
):
|
||||
# parse a nested requirements file
|
||||
if line.opts.requirements:
|
||||
req_path = line.opts.requirements[0]
|
||||
nested_constraint = False
|
||||
else:
|
||||
req_path = line.opts.constraints[0]
|
||||
nested_constraint = True
|
||||
|
||||
# original file is over http
|
||||
if SCHEME_RE.search(filename):
|
||||
# do a url join so relative paths work
|
||||
req_path = urllib_parse.urljoin(filename, req_path)
|
||||
# original file and nested file are paths
|
||||
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,
|
||||
)
|
||||
|
||||
for inner_line in self._parse_and_recurse(
|
||||
req_path, nested_constraint,
|
||||
):
|
||||
yield inner_line
|
||||
else:
|
||||
yield line
|
||||
|
||||
def _parse_file(self, filename, constraint):
|
||||
# type: (str, bool) -> Iterator[ParsedLine]
|
||||
_, content = get_file_content(
|
||||
filename, self._session, comes_from=self._comes_from
|
||||
)
|
||||
|
||||
lines_enum = preprocess(content, self._skip_requirements_regex)
|
||||
|
||||
for line_number, line in lines_enum:
|
||||
try:
|
||||
args_str, opts = self._line_parser(line)
|
||||
except OptionParsingError as e:
|
||||
# add offending line
|
||||
msg = 'Invalid requirement: %s\n%s' % (line, e.msg)
|
||||
raise RequirementsFileParseError(msg)
|
||||
|
||||
yield ParsedLine(
|
||||
filename,
|
||||
line_number,
|
||||
self._comes_from,
|
||||
args_str,
|
||||
opts,
|
||||
constraint,
|
||||
)
|
||||
|
||||
|
||||
def get_line_parser(finder):
|
||||
# type: (Optional[PackageFinder]) -> LineParser
|
||||
def parse_line(line):
|
||||
# type: (Text) -> Tuple[str, Values]
|
||||
# Build new parser for each line since it accumulates appendable
|
||||
# options.
|
||||
parser = build_parser()
|
||||
defaults = parser.get_default_values()
|
||||
defaults.index_url = None
|
||||
if finder:
|
||||
defaults.format_control = finder.format_control
|
||||
|
||||
args_str, options_str = break_args_options(line)
|
||||
# Prior to 2.7.3, shlex cannot deal with unicode entries
|
||||
if sys.version_info < (2, 7, 3):
|
||||
# https://github.com/python/mypy/issues/1174
|
||||
options_str = options_str.encode('utf8') # type: ignore
|
||||
|
||||
# https://github.com/python/mypy/issues/1174
|
||||
opts, _ = parser.parse_args(
|
||||
shlex.split(options_str), defaults) # type: ignore
|
||||
|
||||
return args_str, opts
|
||||
|
||||
return parse_line
|
||||
|
||||
|
||||
def break_args_options(line):
|
||||
@@ -293,8 +387,14 @@ def break_args_options(line):
|
||||
return ' '.join(args), ' '.join(options) # type: ignore
|
||||
|
||||
|
||||
def build_parser(line):
|
||||
# type: (Text) -> optparse.OptionParser
|
||||
class OptionParsingError(Exception):
|
||||
def __init__(self, msg):
|
||||
# type: (str) -> None
|
||||
self.msg = msg
|
||||
|
||||
|
||||
def build_parser():
|
||||
# type: () -> optparse.OptionParser
|
||||
"""
|
||||
Return a parser for parsing requirement lines
|
||||
"""
|
||||
@@ -309,9 +409,7 @@ def build_parser(line):
|
||||
# that in our own exception.
|
||||
def parser_exit(self, msg):
|
||||
# type: (Any, str) -> NoReturn
|
||||
# add offending line
|
||||
msg = 'Invalid requirement: %s\n%s' % (line, msg)
|
||||
raise RequirementsFileParseError(msg)
|
||||
raise OptionParsingError(msg)
|
||||
# NOTE: mypy disallows assigning to a method
|
||||
# https://github.com/python/mypy/issues/2427
|
||||
parser.exit = parser_exit # type: ignore
|
||||
@@ -361,17 +459,15 @@ def ignore_comments(lines_enum):
|
||||
yield line_number, line
|
||||
|
||||
|
||||
def skip_regex(lines_enum, options):
|
||||
# type: (ReqFileLines, Optional[optparse.Values]) -> ReqFileLines
|
||||
def skip_regex(lines_enum, pattern):
|
||||
# type: (ReqFileLines, str) -> ReqFileLines
|
||||
"""
|
||||
Skip lines that match '--skip-requirements-regex' pattern
|
||||
Skip lines that match the provided pattern
|
||||
|
||||
Note: the regex pattern is only built once
|
||||
"""
|
||||
skip_regex = options.skip_requirements_regex if options else None
|
||||
if skip_regex:
|
||||
pattern = re.compile(skip_regex)
|
||||
lines_enum = filterfalse(lambda e: pattern.search(e[1]), lines_enum)
|
||||
matcher = re.compile(pattern)
|
||||
lines_enum = filterfalse(lambda e: matcher.search(e[1]), lines_enum)
|
||||
return lines_enum
|
||||
|
||||
|
||||
@@ -401,3 +497,50 @@ def expand_env_variables(lines_enum):
|
||||
line = line.replace(env_var, value)
|
||||
|
||||
yield line_number, line
|
||||
|
||||
|
||||
def get_file_content(url, session, comes_from=None):
|
||||
# type: (str, PipSession, Optional[str]) -> Tuple[str, Text]
|
||||
"""Gets the content of a file; it may be a filename, file: URL, or
|
||||
http: URL. Returns (location, content). Content is unicode.
|
||||
Respects # -*- coding: declarations on the retrieved files.
|
||||
|
||||
:param url: File path or url.
|
||||
:param session: PipSession instance.
|
||||
:param comes_from: Origin description of requirements.
|
||||
"""
|
||||
scheme = get_url_scheme(url)
|
||||
|
||||
if scheme in ['http', 'https']:
|
||||
# FIXME: catch some errors
|
||||
resp = session.get(url)
|
||||
resp.raise_for_status()
|
||||
return resp.url, resp.text
|
||||
|
||||
elif scheme == 'file':
|
||||
if comes_from and comes_from.startswith('http'):
|
||||
raise InstallationError(
|
||||
'Requirements file %s references URL %s, which is local'
|
||||
% (comes_from, url))
|
||||
|
||||
path = url.split(':', 1)[1]
|
||||
path = path.replace('\\', '/')
|
||||
match = _url_slash_drive_re.match(path)
|
||||
if match:
|
||||
path = match.group(1) + ':' + path.split('|', 1)[1]
|
||||
path = urllib_parse.unquote(path)
|
||||
if path.startswith('/'):
|
||||
path = '/' + path.lstrip('/')
|
||||
url = path
|
||||
|
||||
try:
|
||||
with open(url, 'rb') as f:
|
||||
content = auto_decode(f.read())
|
||||
except IOError as exc:
|
||||
raise InstallationError(
|
||||
'Could not open requirements file: %s' % str(exc)
|
||||
)
|
||||
return url, content
|
||||
|
||||
|
||||
_url_slash_drive_re = re.compile(r'/*([a-z])\|', re.I)
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
# The following comment should be removed at some point in the future.
|
||||
# mypy: strict-optional=False
|
||||
# mypy: disallow-untyped-defs=False
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import atexit
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import sysconfig
|
||||
import zipfile
|
||||
from distutils.util import change_root
|
||||
|
||||
from pipenv.patched.notpip._vendor import pkg_resources, six
|
||||
from pipenv.patched.notpip._vendor.packaging.requirements import Requirement
|
||||
@@ -20,39 +16,40 @@ 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._internal import pep425tags, wheel
|
||||
from pipenv.patched.notpip._internal import pep425tags
|
||||
from pipenv.patched.notpip._internal.build_env import NoOpBuildEnvironment
|
||||
from pipenv.patched.notpip._internal.exceptions import InstallationError
|
||||
from pipenv.patched.notpip._internal.locations import get_scheme
|
||||
from pipenv.patched.notpip._internal.models.link import Link
|
||||
from pipenv.patched.notpip._internal.operations.generate_metadata import get_metadata_generator
|
||||
from pipenv.patched.notpip._internal.operations.build.metadata import generate_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 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.compat import native_str
|
||||
from pipenv.patched.notpip._internal.utils.deprecation import deprecated
|
||||
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.marker_files import (
|
||||
PIP_DELETE_MARKER_FILENAME,
|
||||
has_delete_marker_file,
|
||||
write_delete_marker_file,
|
||||
)
|
||||
from pipenv.patched.notpip._internal.utils.misc import (
|
||||
_make_build_dir,
|
||||
ask_path_exists,
|
||||
backup_dir,
|
||||
display_path,
|
||||
dist_in_site_packages,
|
||||
dist_in_usersite,
|
||||
ensure_dir,
|
||||
get_installed_version,
|
||||
hide_url,
|
||||
redact_auth_from_url,
|
||||
rmtree,
|
||||
)
|
||||
from pipenv.patched.notpip._internal.utils.packaging import get_metadata
|
||||
from pipenv.patched.notpip._internal.utils.setuptools_build import make_setuptools_shim_args
|
||||
from pipenv.patched.notpip._internal.utils.subprocess import (
|
||||
call_subprocess,
|
||||
runner_with_spinner_message,
|
||||
)
|
||||
from pipenv.patched.notpip._internal.utils.temp_dir import TempDirectory
|
||||
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
|
||||
from pipenv.patched.notpip._internal.utils.virtualenv import running_under_virtualenv
|
||||
@@ -64,7 +61,7 @@ if MYPY_CHECK_RUNNING:
|
||||
)
|
||||
from pipenv.patched.notpip._internal.build_env import BuildEnvironment
|
||||
from pipenv.patched.notpip._internal.cache import WheelCache
|
||||
from pipenv.patched.notpip._internal.index import PackageFinder
|
||||
from pipenv.patched.notpip._internal.index.package_finder import PackageFinder
|
||||
from pipenv.patched.notpip._vendor.pkg_resources import Distribution
|
||||
from pipenv.patched.notpip._vendor.packaging.specifiers import SpecifierSet
|
||||
from pipenv.patched.notpip._vendor.packaging.markers import Marker
|
||||
@@ -73,6 +70,32 @@ if MYPY_CHECK_RUNNING:
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _get_dist(metadata_directory):
|
||||
# type: (str) -> Distribution
|
||||
"""Return a pkg_resources.Distribution for the provided
|
||||
metadata directory.
|
||||
"""
|
||||
dist_dir = metadata_directory.rstrip(os.sep)
|
||||
|
||||
# Determine the correct Distribution object type.
|
||||
if dist_dir.endswith(".egg-info"):
|
||||
dist_cls = pkg_resources.Distribution
|
||||
else:
|
||||
assert dist_dir.endswith(".dist-info")
|
||||
dist_cls = pkg_resources.DistInfoDistribution
|
||||
|
||||
# Build a PathMetadata object, from path to metadata. :wink:
|
||||
base_dir, dist_dir_name = os.path.split(dist_dir)
|
||||
dist_name = os.path.splitext(dist_dir_name)[0]
|
||||
metadata = pkg_resources.PathMetadata(base_dir, dist_dir)
|
||||
|
||||
return dist_cls(
|
||||
base_dir,
|
||||
project_name=dist_name,
|
||||
metadata=metadata,
|
||||
)
|
||||
|
||||
|
||||
class InstallRequirement(object):
|
||||
"""
|
||||
Represents something that may be installed later on, may have information
|
||||
@@ -111,6 +134,10 @@ class InstallRequirement(object):
|
||||
# PEP 508 URL requirement
|
||||
link = Link(req.url)
|
||||
self.link = self.original_link = link
|
||||
# Path to any downloaded or already-existing package.
|
||||
self.local_file_path = None # type: Optional[str]
|
||||
if self.link and self.link.is_file:
|
||||
self.local_file_path = self.link.file_path
|
||||
|
||||
if extras:
|
||||
self.extras = extras
|
||||
@@ -126,15 +153,12 @@ class InstallRequirement(object):
|
||||
|
||||
# This holds the pkg_resources.Distribution object if this requirement
|
||||
# is already available:
|
||||
self.satisfied_by = None
|
||||
# This hold the pkg_resources.Distribution object if this requirement
|
||||
# conflicts with another installed distribution:
|
||||
self.conflicts_with = None
|
||||
self.satisfied_by = None # type: Optional[Distribution]
|
||||
# Whether the installation process should try to uninstall an existing
|
||||
# distribution before installing this requirement.
|
||||
self.should_reinstall = False
|
||||
# Temporary build location
|
||||
self._temp_build_dir = None # type: Optional[TempDirectory]
|
||||
# Used to store the global directory where the _temp_build_dir should
|
||||
# have been created. Cf move_to_correct_build_directory method.
|
||||
self._ideal_build_dir = None # type: Optional[str]
|
||||
# Set to True after successful installation
|
||||
self.install_succeeded = None # type: Optional[bool]
|
||||
self.options = options if options else {}
|
||||
@@ -240,7 +264,7 @@ class InstallRequirement(object):
|
||||
# type: () -> Optional[str]
|
||||
if self.req is None:
|
||||
return None
|
||||
return native_str(pkg_resources.safe_name(self.req.name))
|
||||
return six.ensure_str(pkg_resources.safe_name(self.req.name))
|
||||
|
||||
@property
|
||||
def specifier(self):
|
||||
@@ -332,15 +356,10 @@ class InstallRequirement(object):
|
||||
assert self._temp_build_dir.path
|
||||
return self._temp_build_dir.path
|
||||
if self.req is None:
|
||||
# for requirement via a path to a directory: the name of the
|
||||
# package is not available yet so we create a temp directory
|
||||
# Once run_egg_info will have run, we'll be able to fix it via
|
||||
# move_to_correct_build_directory().
|
||||
# Some systems have /tmp as a symlink which confuses custom
|
||||
# builds (such as numpy). Thus, we ensure that the real path
|
||||
# is returned.
|
||||
self._temp_build_dir = TempDirectory(kind="req-build")
|
||||
self._ideal_build_dir = build_dir
|
||||
|
||||
return self._temp_build_dir.path
|
||||
if self.editable:
|
||||
@@ -351,64 +370,47 @@ class InstallRequirement(object):
|
||||
# need this)
|
||||
if not os.path.exists(build_dir):
|
||||
logger.debug('Creating directory %s', build_dir)
|
||||
_make_build_dir(build_dir)
|
||||
os.makedirs(build_dir)
|
||||
write_delete_marker_file(build_dir)
|
||||
return os.path.join(build_dir, name)
|
||||
|
||||
def move_to_correct_build_directory(self):
|
||||
def _set_requirement(self):
|
||||
# type: () -> None
|
||||
"""Move self._temp_build_dir to "self._ideal_build_dir/self.req.name"
|
||||
|
||||
For some requirements (e.g. a path to a directory), the name of the
|
||||
package is not available until we run egg_info, so the build_location
|
||||
will return a temporary directory and store the _ideal_build_dir.
|
||||
|
||||
This is only called to "fix" the build directory after generating
|
||||
metadata.
|
||||
"""Set requirement after generating metadata.
|
||||
"""
|
||||
if self.source_dir is not None:
|
||||
assert self.req is None
|
||||
assert self.metadata is not None
|
||||
assert self.source_dir is not None
|
||||
|
||||
# Construct a Requirement object from the generated metadata
|
||||
if isinstance(parse_version(self.metadata["Version"]), Version):
|
||||
op = "=="
|
||||
else:
|
||||
op = "==="
|
||||
|
||||
self.req = Requirement(
|
||||
"".join([
|
||||
self.metadata["Name"],
|
||||
op,
|
||||
self.metadata["Version"],
|
||||
])
|
||||
)
|
||||
|
||||
def warn_on_mismatching_name(self):
|
||||
# type: () -> None
|
||||
metadata_name = canonicalize_name(self.metadata["Name"])
|
||||
if canonicalize_name(self.req.name) == metadata_name:
|
||||
# Everything is fine.
|
||||
return
|
||||
assert self.req is not None
|
||||
assert self._temp_build_dir
|
||||
assert (
|
||||
self._ideal_build_dir is not None and
|
||||
self._ideal_build_dir.path # type: ignore
|
||||
|
||||
# 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
|
||||
)
|
||||
old_location = self._temp_build_dir
|
||||
self._temp_build_dir = None # checked inside ensure_build_location
|
||||
|
||||
# Figure out the correct place to put the files.
|
||||
new_location = self.ensure_build_location(self._ideal_build_dir)
|
||||
if os.path.exists(new_location):
|
||||
raise InstallationError(
|
||||
'A package already exists in %s; please remove it to continue'
|
||||
% display_path(new_location)
|
||||
)
|
||||
|
||||
# Move the files to the correct location.
|
||||
logger.debug(
|
||||
'Moving package %s from %s to new location %s',
|
||||
self, display_path(old_location.path), display_path(new_location),
|
||||
)
|
||||
shutil.move(old_location.path, new_location)
|
||||
|
||||
# Update directory-tracking variables, to be in line with new_location
|
||||
self.source_dir = os.path.normpath(os.path.abspath(new_location))
|
||||
self._temp_build_dir = TempDirectory(
|
||||
path=new_location, kind="req-install",
|
||||
)
|
||||
|
||||
# Correct the metadata directory, if it exists
|
||||
if self.metadata_directory:
|
||||
old_meta = self.metadata_directory
|
||||
rel = os.path.relpath(old_meta, start=old_location.path)
|
||||
new_meta = os.path.join(new_location, rel)
|
||||
new_meta = os.path.normpath(os.path.abspath(new_meta))
|
||||
self.metadata_directory = new_meta
|
||||
|
||||
# Done with any "move built files" work, since have moved files to the
|
||||
# "ideal" build location. Setting to None allows to clearly flag that
|
||||
# no more moves are needed.
|
||||
self._ideal_build_dir = None
|
||||
self.req = Requirement(metadata_name)
|
||||
|
||||
def remove_temporary_source(self):
|
||||
# type: () -> None
|
||||
@@ -424,36 +426,30 @@ class InstallRequirement(object):
|
||||
self.build_env.cleanup()
|
||||
|
||||
def check_if_exists(self, use_user_site):
|
||||
# type: (bool) -> bool
|
||||
# type: (bool) -> None
|
||||
"""Find an installed distribution that satisfies or conflicts
|
||||
with this requirement, and set self.satisfied_by or
|
||||
self.conflicts_with appropriately.
|
||||
self.should_reinstall appropriately.
|
||||
"""
|
||||
if self.req is None:
|
||||
return False
|
||||
return
|
||||
# get_distribution() will resolve the entire list of requirements
|
||||
# anyway, and we've already determined that we need the requirement
|
||||
# in question, so strip the marker so that we don't try to
|
||||
# evaluate it.
|
||||
no_marker = Requirement(str(self.req))
|
||||
no_marker.marker = None
|
||||
try:
|
||||
# get_distribution() will resolve the entire list of requirements
|
||||
# anyway, and we've already determined that we need the requirement
|
||||
# in question, so strip the marker so that we don't try to
|
||||
# evaluate it.
|
||||
no_marker = Requirement(str(self.req))
|
||||
no_marker.marker = None
|
||||
self.satisfied_by = pkg_resources.get_distribution(str(no_marker))
|
||||
if self.editable and self.satisfied_by:
|
||||
self.conflicts_with = self.satisfied_by
|
||||
# when installing editables, nothing pre-existing should ever
|
||||
# satisfy
|
||||
self.satisfied_by = None
|
||||
return True
|
||||
except pkg_resources.DistributionNotFound:
|
||||
return False
|
||||
return
|
||||
except pkg_resources.VersionConflict:
|
||||
existing_dist = pkg_resources.get_distribution(
|
||||
self.req.name
|
||||
)
|
||||
if use_user_site:
|
||||
if dist_in_usersite(existing_dist):
|
||||
self.conflicts_with = existing_dist
|
||||
self.should_reinstall = True
|
||||
elif (running_under_virtualenv() and
|
||||
dist_in_site_packages(existing_dist)):
|
||||
raise InstallationError(
|
||||
@@ -462,8 +458,13 @@ class InstallRequirement(object):
|
||||
(existing_dist.project_name, existing_dist.location)
|
||||
)
|
||||
else:
|
||||
self.conflicts_with = existing_dist
|
||||
return True
|
||||
self.should_reinstall = True
|
||||
else:
|
||||
if self.editable and self.satisfied_by:
|
||||
self.should_reinstall = True
|
||||
# when installing editables, nothing pre-existing should ever
|
||||
# satisfy
|
||||
self.satisfied_by = None
|
||||
|
||||
# Things valid for wheels
|
||||
@property
|
||||
@@ -473,28 +474,6 @@ class InstallRequirement(object):
|
||||
return False
|
||||
return self.link.is_wheel
|
||||
|
||||
def move_wheel_files(
|
||||
self,
|
||||
wheeldir, # type: str
|
||||
root=None, # type: Optional[str]
|
||||
home=None, # type: Optional[str]
|
||||
prefix=None, # type: Optional[str]
|
||||
warn_script_location=True, # type: bool
|
||||
use_user_site=False, # type: bool
|
||||
pycompile=True # type: bool
|
||||
):
|
||||
# type: (...) -> None
|
||||
wheel.move_wheel_files(
|
||||
self.name, self.req, wheeldir,
|
||||
user=use_user_site,
|
||||
home=home,
|
||||
root=root,
|
||||
prefix=prefix,
|
||||
pycompile=pycompile,
|
||||
isolated=self.isolated,
|
||||
warn_script_location=warn_script_location,
|
||||
)
|
||||
|
||||
# Things valid for sdists
|
||||
@property
|
||||
def unpacked_source_directory(self):
|
||||
@@ -542,11 +521,34 @@ class InstallRequirement(object):
|
||||
return
|
||||
|
||||
self.use_pep517 = True
|
||||
requires, backend, check = pyproject_toml_data
|
||||
requires, backend, check, backend_path = pyproject_toml_data
|
||||
self.requirements_to_check = check
|
||||
self.pyproject_requires = requires
|
||||
self.pep517_backend = Pep517HookCaller(
|
||||
self.unpacked_source_directory, backend
|
||||
self.unpacked_source_directory, backend, backend_path=backend_path,
|
||||
)
|
||||
|
||||
def _generate_metadata(self):
|
||||
# type: () -> str
|
||||
"""Invokes metadata generator functions, with the required arguments.
|
||||
"""
|
||||
if not self.use_pep517:
|
||||
assert self.unpacked_source_directory
|
||||
|
||||
return generate_metadata_legacy(
|
||||
build_env=self.build_env,
|
||||
setup_py_path=self.setup_py_path,
|
||||
source_dir=self.unpacked_source_directory,
|
||||
editable=self.editable,
|
||||
isolated=self.isolated,
|
||||
details=self.name or "from {}".format(self.link)
|
||||
)
|
||||
|
||||
assert self.pep517_backend is not None
|
||||
|
||||
return generate_metadata(
|
||||
build_env=self.build_env,
|
||||
backend=self.pep517_backend,
|
||||
)
|
||||
|
||||
def prepare_metadata(self):
|
||||
@@ -558,56 +560,16 @@ class InstallRequirement(object):
|
||||
"""
|
||||
assert self.source_dir
|
||||
|
||||
metadata_generator = get_metadata_generator(self)
|
||||
with indent_log():
|
||||
self.metadata_directory = metadata_generator(self)
|
||||
self.metadata_directory = self._generate_metadata()
|
||||
|
||||
if not self.req:
|
||||
if isinstance(parse_version(self.metadata["Version"]), Version):
|
||||
op = "=="
|
||||
else:
|
||||
op = "==="
|
||||
self.req = Requirement(
|
||||
"".join([
|
||||
self.metadata["Name"],
|
||||
op,
|
||||
self.metadata["Version"],
|
||||
])
|
||||
)
|
||||
self.move_to_correct_build_directory()
|
||||
# Act on the newly generated metadata, based on the name and version.
|
||||
if not self.name:
|
||||
self._set_requirement()
|
||||
else:
|
||||
metadata_name = canonicalize_name(self.metadata["Name"])
|
||||
if canonicalize_name(self.req.name) != metadata_name:
|
||||
logger.warning(
|
||||
'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)
|
||||
self.warn_on_mismatching_name()
|
||||
|
||||
def prepare_pep517_metadata(self):
|
||||
# type: () -> str
|
||||
assert self.pep517_backend is not None
|
||||
|
||||
# NOTE: This needs to be refactored to stop using atexit
|
||||
metadata_tmpdir = TempDirectory(kind="modern-metadata")
|
||||
atexit.register(metadata_tmpdir.cleanup)
|
||||
|
||||
metadata_dir = metadata_tmpdir.path
|
||||
|
||||
with self.build_env:
|
||||
# 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")
|
||||
backend = self.pep517_backend
|
||||
with backend.subprocess_runner(runner):
|
||||
distinfo_dir = backend.prepare_metadata_for_build_wheel(
|
||||
metadata_dir
|
||||
)
|
||||
|
||||
return os.path.join(metadata_dir, distinfo_dir)
|
||||
self.assert_source_matches_version()
|
||||
|
||||
@property
|
||||
def metadata(self):
|
||||
@@ -619,26 +581,7 @@ class InstallRequirement(object):
|
||||
|
||||
def get_dist(self):
|
||||
# type: () -> Distribution
|
||||
"""Return a pkg_resources.Distribution for this requirement"""
|
||||
dist_dir = self.metadata_directory.rstrip(os.sep)
|
||||
|
||||
# Determine the correct Distribution object type.
|
||||
if dist_dir.endswith(".egg-info"):
|
||||
dist_cls = pkg_resources.Distribution
|
||||
else:
|
||||
assert dist_dir.endswith(".dist-info")
|
||||
dist_cls = pkg_resources.DistInfoDistribution
|
||||
|
||||
# Build a PathMetadata object, from path to metadata. :wink:
|
||||
base_dir, dist_dir_name = os.path.split(dist_dir)
|
||||
dist_name = os.path.splitext(dist_dir_name)[0]
|
||||
metadata = pkg_resources.PathMetadata(base_dir, dist_dir)
|
||||
|
||||
return dist_cls(
|
||||
base_dir,
|
||||
project_name=dist_name,
|
||||
metadata=metadata,
|
||||
)
|
||||
return _get_dist(self.metadata_directory)
|
||||
|
||||
def assert_source_matches_version(self):
|
||||
# type: () -> None
|
||||
@@ -674,34 +617,6 @@ class InstallRequirement(object):
|
||||
self.source_dir = self.ensure_build_location(parent_dir)
|
||||
|
||||
# For editable installations
|
||||
def install_editable(
|
||||
self,
|
||||
install_options, # type: List[str]
|
||||
global_options=(), # type: Sequence[str]
|
||||
prefix=None # type: Optional[str]
|
||||
):
|
||||
# type: (...) -> None
|
||||
logger.info('Running setup.py develop for %s', self.name)
|
||||
|
||||
if prefix:
|
||||
prefix_param = ['--prefix={}'.format(prefix)]
|
||||
install_options = list(install_options) + prefix_param
|
||||
base_cmd = make_setuptools_shim_args(
|
||||
self.setup_py_path,
|
||||
global_options=global_options,
|
||||
no_user_config=self.isolated
|
||||
)
|
||||
with indent_log():
|
||||
with self.build_env:
|
||||
call_subprocess(
|
||||
base_cmd +
|
||||
['develop', '--no-deps'] +
|
||||
list(install_options),
|
||||
cwd=self.unpacked_source_directory,
|
||||
)
|
||||
|
||||
self.install_succeeded = True
|
||||
|
||||
def update_editable(self, obtain=True):
|
||||
# type: (bool) -> None
|
||||
if not self.link:
|
||||
@@ -720,6 +635,20 @@ class InstallRequirement(object):
|
||||
vc_type, url = self.link.url.split('+', 1)
|
||||
vcs_backend = vcs.get_backend(vc_type)
|
||||
if vcs_backend:
|
||||
if not self.link.is_vcs:
|
||||
reason = (
|
||||
"This form of VCS requirement is being deprecated: {}."
|
||||
).format(
|
||||
self.link.url
|
||||
)
|
||||
replacement = None
|
||||
if self.link.url.startswith("git+git@"):
|
||||
replacement = (
|
||||
"git+https://git@example.com/..., "
|
||||
"git+ssh://git@example.com/..., "
|
||||
"or the insecure git+git://git@example.com/..."
|
||||
)
|
||||
deprecated(reason, replacement, gone_in="21.0", issue=7554)
|
||||
hidden_url = hide_url(self.link.url)
|
||||
if obtain:
|
||||
vcs_backend.obtain(self.source_dir, url=hidden_url)
|
||||
@@ -731,9 +660,8 @@ class InstallRequirement(object):
|
||||
% (self.link, vc_type))
|
||||
|
||||
# Top-level Actions
|
||||
def uninstall(self, auto_confirm=False, verbose=False,
|
||||
use_user_site=False):
|
||||
# type: (bool, bool, bool) -> Optional[UninstallPathSet]
|
||||
def uninstall(self, auto_confirm=False, verbose=False):
|
||||
# type: (bool, bool) -> Optional[UninstallPathSet]
|
||||
"""
|
||||
Uninstall the distribution currently satisfying this requirement.
|
||||
|
||||
@@ -746,28 +674,33 @@ class InstallRequirement(object):
|
||||
linked to global site-packages.
|
||||
|
||||
"""
|
||||
if not self.check_if_exists(use_user_site):
|
||||
assert self.req
|
||||
try:
|
||||
dist = pkg_resources.get_distribution(self.req.name)
|
||||
except pkg_resources.DistributionNotFound:
|
||||
logger.warning("Skipping %s as it is not installed.", self.name)
|
||||
return None
|
||||
dist = self.satisfied_by or self.conflicts_with
|
||||
else:
|
||||
logger.info('Found existing installation: %s', dist)
|
||||
|
||||
uninstalled_pathset = UninstallPathSet.from_dist(dist)
|
||||
uninstalled_pathset.remove(auto_confirm, verbose)
|
||||
return uninstalled_pathset
|
||||
|
||||
def _clean_zip_name(self, name, prefix): # only used by archive.
|
||||
# type: (str, str) -> str
|
||||
assert name.startswith(prefix + os.path.sep), (
|
||||
"name %r doesn't start with prefix %r" % (name, prefix)
|
||||
)
|
||||
name = name[len(prefix) + 1:]
|
||||
name = name.replace(os.path.sep, '/')
|
||||
return name
|
||||
|
||||
def _get_archive_name(self, path, parentdir, rootdir):
|
||||
# type: (str, str, str) -> str
|
||||
|
||||
def _clean_zip_name(name, prefix):
|
||||
# type: (str, str) -> str
|
||||
assert name.startswith(prefix + os.path.sep), (
|
||||
"name %r doesn't start with prefix %r" % (name, prefix)
|
||||
)
|
||||
name = name[len(prefix) + 1:]
|
||||
name = name.replace(os.path.sep, '/')
|
||||
return name
|
||||
|
||||
path = os.path.join(parentdir, path)
|
||||
name = self._clean_zip_name(path, rootdir)
|
||||
name = _clean_zip_name(path, rootdir)
|
||||
return self.name + '/' + name
|
||||
|
||||
def archive(self, build_dir):
|
||||
@@ -845,122 +778,53 @@ class InstallRequirement(object):
|
||||
pycompile=True # type: bool
|
||||
):
|
||||
# type: (...) -> None
|
||||
scheme = get_scheme(
|
||||
self.name,
|
||||
user=use_user_site,
|
||||
home=home,
|
||||
root=root,
|
||||
isolated=self.isolated,
|
||||
prefix=prefix,
|
||||
)
|
||||
|
||||
global_options = global_options if global_options is not None else []
|
||||
if self.editable:
|
||||
self.install_editable(
|
||||
install_options, global_options, prefix=prefix,
|
||||
install_editable_legacy(
|
||||
install_options,
|
||||
global_options,
|
||||
prefix=prefix,
|
||||
home=home,
|
||||
use_user_site=use_user_site,
|
||||
name=self.name,
|
||||
setup_py_path=self.setup_py_path,
|
||||
isolated=self.isolated,
|
||||
build_env=self.build_env,
|
||||
unpacked_source_directory=self.unpacked_source_directory,
|
||||
)
|
||||
self.install_succeeded = True
|
||||
return
|
||||
|
||||
if self.is_wheel:
|
||||
version = wheel.wheel_version(self.source_dir)
|
||||
wheel.check_compatibility(version, self.name)
|
||||
|
||||
self.move_wheel_files(
|
||||
self.source_dir, root=root, prefix=prefix, home=home,
|
||||
assert self.local_file_path
|
||||
install_wheel(
|
||||
self.name,
|
||||
self.local_file_path,
|
||||
scheme=scheme,
|
||||
req_description=str(self.req),
|
||||
pycompile=pycompile,
|
||||
warn_script_location=warn_script_location,
|
||||
use_user_site=use_user_site, pycompile=pycompile,
|
||||
)
|
||||
self.install_succeeded = True
|
||||
return
|
||||
|
||||
# Extend the list of global and install options passed on to
|
||||
# the setup.py call with the ones from the requirements file.
|
||||
# Options specified in requirements file override those
|
||||
# specified on the command line, since the last option given
|
||||
# to setup.py is the one that is used.
|
||||
global_options = list(global_options) + \
|
||||
self.options.get('global_options', [])
|
||||
install_options = list(install_options) + \
|
||||
self.options.get('install_options', [])
|
||||
|
||||
with TempDirectory(kind="record") as temp_dir:
|
||||
record_filename = os.path.join(temp_dir.path, 'install-record.txt')
|
||||
install_args = self.get_install_args(
|
||||
global_options, record_filename, root, prefix, pycompile,
|
||||
)
|
||||
|
||||
runner = runner_with_spinner_message(
|
||||
"Running setup.py install for {}".format(self.name)
|
||||
)
|
||||
with indent_log(), self.build_env:
|
||||
runner(
|
||||
cmd=install_args + install_options,
|
||||
cwd=self.unpacked_source_directory,
|
||||
)
|
||||
|
||||
if not os.path.exists(record_filename):
|
||||
logger.debug('Record file %s not found', record_filename)
|
||||
return
|
||||
self.install_succeeded = True
|
||||
|
||||
def prepend_root(path):
|
||||
# type: (str) -> str
|
||||
if root is None or not os.path.isabs(path):
|
||||
return path
|
||||
else:
|
||||
return change_root(root, path)
|
||||
|
||||
with open(record_filename) as f:
|
||||
for line in f:
|
||||
directory = os.path.dirname(line)
|
||||
if directory.endswith('.egg-info'):
|
||||
egg_info_dir = prepend_root(directory)
|
||||
break
|
||||
else:
|
||||
logger.warning(
|
||||
'Could not find .egg-info directory in install record'
|
||||
' for %s',
|
||||
self,
|
||||
)
|
||||
# FIXME: put the record somewhere
|
||||
return
|
||||
new_lines = []
|
||||
with open(record_filename) as f:
|
||||
for line in f:
|
||||
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.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')
|
||||
|
||||
def get_install_args(
|
||||
self,
|
||||
global_options, # type: Sequence[str]
|
||||
record_filename, # type: str
|
||||
root, # type: Optional[str]
|
||||
prefix, # type: Optional[str]
|
||||
pycompile # type: bool
|
||||
):
|
||||
# type: (...) -> List[str]
|
||||
install_args = make_setuptools_shim_args(
|
||||
self.setup_py_path,
|
||||
install_legacy(
|
||||
self,
|
||||
install_options=install_options,
|
||||
global_options=global_options,
|
||||
no_user_config=self.isolated,
|
||||
unbuffered_output=True
|
||||
root=root,
|
||||
home=home,
|
||||
prefix=prefix,
|
||||
use_user_site=use_user_site,
|
||||
pycompile=pycompile,
|
||||
scheme=scheme,
|
||||
)
|
||||
install_args += ['install', '--record', record_filename]
|
||||
install_args += ['--single-version-externally-managed']
|
||||
|
||||
if root is not None:
|
||||
install_args += ['--root', root]
|
||||
if prefix is not None:
|
||||
install_args += ['--prefix', prefix]
|
||||
|
||||
if pycompile:
|
||||
install_args += ["--compile"]
|
||||
else:
|
||||
install_args += ["--no-compile"]
|
||||
|
||||
if running_under_virtualenv():
|
||||
py_ver_str = 'python' + sysconfig.get_python_version()
|
||||
install_args += ['--install-headers',
|
||||
os.path.join(sys.prefix, 'include', 'site',
|
||||
py_ver_str, self.name)]
|
||||
|
||||
return install_args
|
||||
|
||||
@@ -10,9 +10,9 @@ from pipenv.patched.notpip._vendor.packaging.utils import canonicalize_name
|
||||
|
||||
from pipenv.patched.notpip._internal import pep425tags
|
||||
from pipenv.patched.notpip._internal.exceptions import InstallationError
|
||||
from pipenv.patched.notpip._internal.models.wheel import Wheel
|
||||
from pipenv.patched.notpip._internal.utils.logging import indent_log
|
||||
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
|
||||
from pipenv.patched.notpip._internal.wheel import Wheel
|
||||
|
||||
if MYPY_CHECK_RUNNING:
|
||||
from typing import Dict, Iterable, List, Optional, Tuple
|
||||
@@ -24,13 +24,12 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
class RequirementSet(object):
|
||||
|
||||
def __init__(self, require_hashes=False, check_supported_wheels=True, ignore_compatibility=True):
|
||||
# type: (bool) -> None
|
||||
def __init__(self, check_supported_wheels=True, ignore_compatibility=True):
|
||||
# type: (bool, bool) -> None
|
||||
"""Create a RequirementSet.
|
||||
"""
|
||||
|
||||
self.requirements = OrderedDict() # type: Dict[str, InstallRequirement] # noqa: E501
|
||||
self.require_hashes = require_hashes
|
||||
self.check_supported_wheels = check_supported_wheels
|
||||
|
||||
self.unnamed_requirements = [] # type: List[InstallRequirement]
|
||||
|
||||
@@ -9,34 +9,74 @@ import hashlib
|
||||
import logging
|
||||
import os
|
||||
|
||||
from pipenv.patched.notpip._vendor import contextlib2
|
||||
|
||||
from pipenv.patched.notpip._internal.utils.temp_dir import TempDirectory
|
||||
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
|
||||
|
||||
if MYPY_CHECK_RUNNING:
|
||||
from types import TracebackType
|
||||
from typing import Iterator, Optional, Set, Type
|
||||
from typing import Dict, Iterator, Optional, Set, Type, Union
|
||||
from pipenv.patched.notpip._internal.req.req_install import InstallRequirement
|
||||
from pipenv.patched.notpip._internal.models.link import Link
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def update_env_context_manager(**changes):
|
||||
# type: (str) -> Iterator[None]
|
||||
target = os.environ
|
||||
|
||||
# Save values from the target and change them.
|
||||
non_existent_marker = object()
|
||||
saved_values = {} # type: Dict[str, Union[object, str]]
|
||||
for name, new_value in changes.items():
|
||||
try:
|
||||
saved_values[name] = target[name]
|
||||
except KeyError:
|
||||
saved_values[name] = non_existent_marker
|
||||
target[name] = new_value
|
||||
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
# Restore original values in the target.
|
||||
for name, original_value in saved_values.items():
|
||||
if original_value is non_existent_marker:
|
||||
del target[name]
|
||||
else:
|
||||
assert isinstance(original_value, str) # for mypy
|
||||
target[name] = original_value
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def get_requirement_tracker():
|
||||
# type: () -> Iterator[RequirementTracker]
|
||||
root = os.environ.get('PIP_REQ_TRACKER')
|
||||
with contextlib2.ExitStack() as ctx:
|
||||
if root is None:
|
||||
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)
|
||||
|
||||
with RequirementTracker(root) as tracker:
|
||||
yield tracker
|
||||
|
||||
|
||||
class RequirementTracker(object):
|
||||
|
||||
def __init__(self):
|
||||
# type: () -> None
|
||||
self._root = os.environ.get('PIP_REQ_TRACKER')
|
||||
if self._root is None:
|
||||
self._temp_dir = TempDirectory(delete=False, kind='req-tracker')
|
||||
self._root = os.environ['PIP_REQ_TRACKER'] = self._temp_dir.path
|
||||
logger.debug('Created requirements tracker %r', self._root)
|
||||
else:
|
||||
self._temp_dir = None
|
||||
logger.debug('Re-using requirements tracker %r', self._root)
|
||||
def __init__(self, root):
|
||||
# type: (str) -> None
|
||||
self._root = root
|
||||
self._entries = set() # type: Set[InstallRequirement]
|
||||
logger.debug("Created build tracker: %s", self._root)
|
||||
|
||||
def __enter__(self):
|
||||
# type: () -> RequirementTracker
|
||||
logger.debug("Entered build tracker: %s", self._root)
|
||||
return self
|
||||
|
||||
def __exit__(
|
||||
@@ -55,40 +95,52 @@ class RequirementTracker(object):
|
||||
|
||||
def add(self, req):
|
||||
# type: (InstallRequirement) -> None
|
||||
link = req.link
|
||||
info = str(req)
|
||||
entry_path = self._entry_path(link)
|
||||
"""Add an InstallRequirement to build tracking.
|
||||
"""
|
||||
|
||||
# Get the file to write information about this requirement.
|
||||
entry_path = self._entry_path(req.link)
|
||||
|
||||
# Try reading from the file. If it exists and can be read from, a build
|
||||
# is already in progress, so a LookupError is raised.
|
||||
try:
|
||||
with open(entry_path) as fp:
|
||||
# Error, these's already a build in progress.
|
||||
raise LookupError('%s is already being built: %s'
|
||||
% (link, fp.read()))
|
||||
contents = fp.read()
|
||||
except IOError as e:
|
||||
# if the error is anything other than "file does not exist", raise.
|
||||
if e.errno != errno.ENOENT:
|
||||
raise
|
||||
assert req not in self._entries
|
||||
with open(entry_path, 'w') as fp:
|
||||
fp.write(info)
|
||||
self._entries.add(req)
|
||||
logger.debug('Added %s to build tracker %r', req, self._root)
|
||||
else:
|
||||
message = '%s is already being built: %s' % (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') as fp:
|
||||
fp.write(str(req))
|
||||
self._entries.add(req)
|
||||
|
||||
logger.debug('Added %s to build tracker %r', req, self._root)
|
||||
|
||||
def remove(self, req):
|
||||
# type: (InstallRequirement) -> None
|
||||
link = req.link
|
||||
"""Remove an InstallRequirement from build tracking.
|
||||
"""
|
||||
|
||||
# Delete the created file and the corresponding entries.
|
||||
os.unlink(self._entry_path(req.link))
|
||||
self._entries.remove(req)
|
||||
os.unlink(self._entry_path(link))
|
||||
|
||||
logger.debug('Removed %s from build tracker %r', req, self._root)
|
||||
|
||||
def cleanup(self):
|
||||
# type: () -> None
|
||||
for req in set(self._entries):
|
||||
self.remove(req)
|
||||
remove = self._temp_dir is not None
|
||||
if remove:
|
||||
self._temp_dir.cleanup()
|
||||
logger.debug('%s build tracker %r',
|
||||
'Removed' if remove else 'Cleaned',
|
||||
self._root)
|
||||
|
||||
logger.debug("Removed build tracker: %r", self._root)
|
||||
|
||||
@contextlib.contextmanager
|
||||
def track(self, req):
|
||||
|
||||
@@ -59,7 +59,7 @@ def _script_names(dist, script_name, is_gui):
|
||||
|
||||
|
||||
def _unique(fn):
|
||||
# type: (Callable) -> Callable[..., Iterator[Any]]
|
||||
# type: (Callable[..., Iterator[Any]]) -> Callable[..., Iterator[Any]]
|
||||
@functools.wraps(fn)
|
||||
def unique(*args, **kw):
|
||||
# type: (Any, Any) -> Iterator[Any]
|
||||
@@ -295,7 +295,7 @@ class StashedUninstallPathSet(object):
|
||||
# type: () -> None
|
||||
"""Undoes the uninstall by moving stashed files back."""
|
||||
for p in self._moves:
|
||||
logging.info("Moving to %s\n from %s", *p)
|
||||
logger.info("Moving to %s\n from %s", *p)
|
||||
|
||||
for new_path, path in self._moves:
|
||||
try:
|
||||
|
||||
@@ -14,11 +14,10 @@ from pipenv.patched.notpip._vendor import pkg_resources
|
||||
from pipenv.patched.notpip._vendor.packaging import version as packaging_version
|
||||
from pipenv.patched.notpip._vendor.six import ensure_binary
|
||||
|
||||
from pipenv.patched.notpip._internal.collector import LinkCollector
|
||||
from pipenv.patched.notpip._internal.index import PackageFinder
|
||||
from pipenv.patched.notpip._internal.index.collector import LinkCollector
|
||||
from pipenv.patched.notpip._internal.index.package_finder import PackageFinder
|
||||
from pipenv.patched.notpip._internal.models.search_scope import SearchScope
|
||||
from pipenv.patched.notpip._internal.models.selection_prefs import SelectionPreferences
|
||||
from pipenv.patched.notpip._internal.utils.compat import WINDOWS
|
||||
from pipenv.patched.notpip._internal.utils.filesystem import (
|
||||
adjacent_tmp_file,
|
||||
check_path_owner,
|
||||
@@ -225,12 +224,11 @@ def pip_self_version_check(session, options):
|
||||
if not local_version_is_older:
|
||||
return
|
||||
|
||||
# Advise "python -m pip" on Windows to avoid issues
|
||||
# with overwriting pip.exe.
|
||||
if WINDOWS:
|
||||
pip_cmd = "python -m pip"
|
||||
else:
|
||||
pip_cmd = "pip"
|
||||
# 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`.
|
||||
pip_cmd = "{} -m pip".format(sys.executable)
|
||||
logger.warning(
|
||||
"You are using pip version %s; however, version %s is "
|
||||
"available.\nYou should consider upgrading via the "
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
"""
|
||||
This code was taken from https://github.com/ActiveState/appdirs and modified
|
||||
to suit our purposes.
|
||||
"""
|
||||
This code wraps the vendored appdirs module to so the return values are
|
||||
compatible for the current pip code base.
|
||||
|
||||
# The following comment should be removed at some point in the future.
|
||||
# mypy: disallow-untyped-defs=False
|
||||
The intention is to rewrite current usages gradually, keeping the tests pass,
|
||||
and eventually drop this after all usages are changed.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from pipenv.patched.notpip._vendor.six import PY2, text_type
|
||||
from pipenv.patched.notpip._vendor import appdirs as _appdirs
|
||||
|
||||
from pipenv.patched.notpip._internal.utils.compat import WINDOWS, expanduser
|
||||
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
|
||||
|
||||
if MYPY_CHECK_RUNNING:
|
||||
@@ -22,255 +20,22 @@ if MYPY_CHECK_RUNNING:
|
||||
|
||||
def user_cache_dir(appname):
|
||||
# type: (str) -> str
|
||||
r"""
|
||||
Return full path to the user-specific cache dir for this application.
|
||||
|
||||
"appname" is the name of application.
|
||||
|
||||
Typical user cache directories are:
|
||||
macOS: ~/Library/Caches/<AppName>
|
||||
Unix: ~/.cache/<AppName> (XDG default)
|
||||
Windows: C:\Users\<username>\AppData\Local\<AppName>\Cache
|
||||
|
||||
On Windows the only suggestion in the MSDN docs is that local settings go
|
||||
in the `CSIDL_LOCAL_APPDATA` directory. This is identical to the
|
||||
non-roaming app data dir (the default returned by `user_data_dir`). Apps
|
||||
typically put cache data somewhere *under* the given dir here. Some
|
||||
examples:
|
||||
...\Mozilla\Firefox\Profiles\<ProfileName>\Cache
|
||||
...\Acme\SuperApp\Cache\1.0
|
||||
|
||||
OPINION: This function appends "Cache" to the `CSIDL_LOCAL_APPDATA` value.
|
||||
"""
|
||||
if WINDOWS:
|
||||
# Get the base path
|
||||
path = os.path.normpath(_get_win_folder("CSIDL_LOCAL_APPDATA"))
|
||||
|
||||
# When using Python 2, return paths as bytes on Windows like we do on
|
||||
# other operating systems. See helper function docs for more details.
|
||||
if PY2 and isinstance(path, text_type):
|
||||
path = _win_path_to_bytes(path)
|
||||
|
||||
# Add our app name and Cache directory to it
|
||||
path = os.path.join(path, appname, "Cache")
|
||||
elif sys.platform == "darwin":
|
||||
# Get the base path
|
||||
path = expanduser("~/Library/Caches")
|
||||
|
||||
# Add our app name to it
|
||||
path = os.path.join(path, appname)
|
||||
else:
|
||||
# Get the base path
|
||||
path = os.getenv("XDG_CACHE_HOME", expanduser("~/.cache"))
|
||||
|
||||
# Add our app name to it
|
||||
path = os.path.join(path, appname)
|
||||
|
||||
return path
|
||||
|
||||
|
||||
def user_data_dir(appname, roaming=False):
|
||||
# type: (str, bool) -> str
|
||||
r"""
|
||||
Return full path to the user-specific data dir for this application.
|
||||
|
||||
"appname" is the name of application.
|
||||
If None, just the system directory is returned.
|
||||
"roaming" (boolean, default False) can be set True to use the Windows
|
||||
roaming appdata directory. That means that for users on a Windows
|
||||
network setup for roaming profiles, this user data will be
|
||||
sync'd on login. See
|
||||
<http://technet.microsoft.com/en-us/library/cc766489(WS.10).aspx>
|
||||
for a discussion of issues.
|
||||
|
||||
Typical user data directories are:
|
||||
macOS: ~/Library/Application Support/<AppName>
|
||||
if it exists, else ~/.config/<AppName>
|
||||
Unix: ~/.local/share/<AppName> # or in
|
||||
$XDG_DATA_HOME, if defined
|
||||
Win XP (not roaming): C:\Documents and Settings\<username>\ ...
|
||||
...Application Data\<AppName>
|
||||
Win XP (roaming): C:\Documents and Settings\<username>\Local ...
|
||||
...Settings\Application Data\<AppName>
|
||||
Win 7 (not roaming): C:\\Users\<username>\AppData\Local\<AppName>
|
||||
Win 7 (roaming): C:\\Users\<username>\AppData\Roaming\<AppName>
|
||||
|
||||
For Unix, we follow the XDG spec and support $XDG_DATA_HOME.
|
||||
That means, by default "~/.local/share/<AppName>".
|
||||
"""
|
||||
if WINDOWS:
|
||||
const = roaming and "CSIDL_APPDATA" or "CSIDL_LOCAL_APPDATA"
|
||||
path = os.path.join(os.path.normpath(_get_win_folder(const)), appname)
|
||||
elif sys.platform == "darwin":
|
||||
path = os.path.join(
|
||||
expanduser('~/Library/Application Support/'),
|
||||
appname,
|
||||
) if os.path.isdir(os.path.join(
|
||||
expanduser('~/Library/Application Support/'),
|
||||
appname,
|
||||
)
|
||||
) else os.path.join(
|
||||
expanduser('~/.config/'),
|
||||
appname,
|
||||
)
|
||||
else:
|
||||
path = os.path.join(
|
||||
os.getenv('XDG_DATA_HOME', expanduser("~/.local/share")),
|
||||
appname,
|
||||
)
|
||||
|
||||
return path
|
||||
return _appdirs.user_cache_dir(appname, appauthor=False)
|
||||
|
||||
|
||||
def user_config_dir(appname, roaming=True):
|
||||
# type: (str, bool) -> str
|
||||
"""Return full path to the user-specific config dir for this application.
|
||||
|
||||
"appname" is the name of application.
|
||||
If None, just the system directory is returned.
|
||||
"roaming" (boolean, default True) can be set False to not use the
|
||||
Windows roaming appdata directory. That means that for users on a
|
||||
Windows network setup for roaming profiles, this user data will be
|
||||
sync'd on login. See
|
||||
<http://technet.microsoft.com/en-us/library/cc766489(WS.10).aspx>
|
||||
for a discussion of issues.
|
||||
|
||||
Typical user data directories are:
|
||||
macOS: same as user_data_dir
|
||||
Unix: ~/.config/<AppName>
|
||||
Win *: same as user_data_dir
|
||||
|
||||
For Unix, we follow the XDG spec and support $XDG_CONFIG_HOME.
|
||||
That means, by default "~/.config/<AppName>".
|
||||
"""
|
||||
if WINDOWS:
|
||||
path = user_data_dir(appname, roaming=roaming)
|
||||
elif sys.platform == "darwin":
|
||||
path = user_data_dir(appname)
|
||||
else:
|
||||
path = os.getenv('XDG_CONFIG_HOME', expanduser("~/.config"))
|
||||
path = os.path.join(path, appname)
|
||||
|
||||
return path
|
||||
return _appdirs.user_config_dir(appname, appauthor=False, roaming=roaming)
|
||||
|
||||
|
||||
def user_data_dir(appname, roaming=False):
|
||||
# type: (str, bool) -> str
|
||||
return _appdirs.user_data_dir(appname, appauthor=False, roaming=roaming)
|
||||
|
||||
|
||||
# for the discussion regarding site_config_dirs locations
|
||||
# see <https://github.com/pypa/pip/issues/1733>
|
||||
def site_config_dirs(appname):
|
||||
# type: (str) -> List[str]
|
||||
r"""Return a list of potential user-shared config dirs for this application.
|
||||
|
||||
"appname" is the name of application.
|
||||
|
||||
Typical user config directories are:
|
||||
macOS: /Library/Application Support/<AppName>/
|
||||
Unix: /etc or $XDG_CONFIG_DIRS[i]/<AppName>/ for each value in
|
||||
$XDG_CONFIG_DIRS
|
||||
Win XP: C:\Documents and Settings\All Users\Application ...
|
||||
...Data\<AppName>\
|
||||
Vista: (Fail! "C:\ProgramData" is a hidden *system* directory
|
||||
on Vista.)
|
||||
Win 7: Hidden, but writeable on Win 7:
|
||||
C:\ProgramData\<AppName>\
|
||||
"""
|
||||
if WINDOWS:
|
||||
path = os.path.normpath(_get_win_folder("CSIDL_COMMON_APPDATA"))
|
||||
pathlist = [os.path.join(path, appname)]
|
||||
elif sys.platform == 'darwin':
|
||||
pathlist = [os.path.join('/Library/Application Support', appname)]
|
||||
else:
|
||||
# try looking in $XDG_CONFIG_DIRS
|
||||
xdg_config_dirs = os.getenv('XDG_CONFIG_DIRS', '/etc/xdg')
|
||||
if xdg_config_dirs:
|
||||
pathlist = [
|
||||
os.path.join(expanduser(x), appname)
|
||||
for x in xdg_config_dirs.split(os.pathsep)
|
||||
]
|
||||
else:
|
||||
pathlist = []
|
||||
|
||||
# always look in /etc directly as well
|
||||
pathlist.append('/etc')
|
||||
|
||||
return pathlist
|
||||
|
||||
|
||||
# -- Windows support functions --
|
||||
|
||||
def _get_win_folder_from_registry(csidl_name):
|
||||
# type: (str) -> str
|
||||
"""
|
||||
This is a fallback technique at best. I'm not sure if using the
|
||||
registry for this guarantees us the correct answer for all CSIDL_*
|
||||
names.
|
||||
"""
|
||||
import _winreg
|
||||
|
||||
shell_folder_name = {
|
||||
"CSIDL_APPDATA": "AppData",
|
||||
"CSIDL_COMMON_APPDATA": "Common AppData",
|
||||
"CSIDL_LOCAL_APPDATA": "Local AppData",
|
||||
}[csidl_name]
|
||||
|
||||
key = _winreg.OpenKey(
|
||||
_winreg.HKEY_CURRENT_USER,
|
||||
r"Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders"
|
||||
)
|
||||
directory, _type = _winreg.QueryValueEx(key, shell_folder_name)
|
||||
return directory
|
||||
|
||||
|
||||
def _get_win_folder_with_ctypes(csidl_name):
|
||||
# type: (str) -> str
|
||||
# On Python 2, ctypes.create_unicode_buffer().value returns "unicode",
|
||||
# which isn't the same as str in the annotation above.
|
||||
csidl_const = {
|
||||
"CSIDL_APPDATA": 26,
|
||||
"CSIDL_COMMON_APPDATA": 35,
|
||||
"CSIDL_LOCAL_APPDATA": 28,
|
||||
}[csidl_name]
|
||||
|
||||
buf = ctypes.create_unicode_buffer(1024)
|
||||
windll = ctypes.windll # type: ignore
|
||||
windll.shell32.SHGetFolderPathW(None, csidl_const, None, 0, buf)
|
||||
|
||||
# Downgrade to short path name if have highbit chars. See
|
||||
# <http://bugs.activestate.com/show_bug.cgi?id=85099>.
|
||||
has_high_char = False
|
||||
for c in buf:
|
||||
if ord(c) > 255:
|
||||
has_high_char = True
|
||||
break
|
||||
if has_high_char:
|
||||
buf2 = ctypes.create_unicode_buffer(1024)
|
||||
if windll.kernel32.GetShortPathNameW(buf.value, buf2, 1024):
|
||||
buf = buf2
|
||||
|
||||
# The type: ignore is explained under the type annotation for this function
|
||||
return buf.value # type: ignore
|
||||
|
||||
|
||||
if WINDOWS:
|
||||
try:
|
||||
import ctypes
|
||||
_get_win_folder = _get_win_folder_with_ctypes
|
||||
except ImportError:
|
||||
_get_win_folder = _get_win_folder_from_registry
|
||||
|
||||
|
||||
def _win_path_to_bytes(path):
|
||||
"""Encode Windows paths to bytes. Only used on Python 2.
|
||||
|
||||
Motivation is to be consistent with other operating systems where paths
|
||||
are also returned as bytes. This avoids problems mixing bytes and Unicode
|
||||
elsewhere in the codebase. For more details and discussion see
|
||||
<https://github.com/pypa/pip/issues/3463>.
|
||||
|
||||
If encoding using ASCII and MBCS fails, return the original Unicode path.
|
||||
"""
|
||||
for encoding in ('ASCII', 'MBCS'):
|
||||
try:
|
||||
return path.encode(encoding)
|
||||
except (UnicodeEncodeError, LookupError):
|
||||
pass
|
||||
return path
|
||||
dirval = _appdirs.site_config_dir(appname, appauthor=False, multipath=True)
|
||||
if _appdirs.system not in ["win32", "darwin"]:
|
||||
return dirval.split(os.pathsep)
|
||||
return [dirval]
|
||||
|
||||
@@ -14,21 +14,12 @@ import shutil
|
||||
import sys
|
||||
|
||||
from pipenv.patched.notpip._vendor.six import PY2, text_type
|
||||
from pipenv.patched.notpip._vendor.urllib3.util import IS_PYOPENSSL
|
||||
|
||||
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
|
||||
|
||||
if MYPY_CHECK_RUNNING:
|
||||
from typing import Optional, Text, Tuple, Union
|
||||
|
||||
try:
|
||||
import _ssl # noqa
|
||||
except ImportError:
|
||||
ssl = None
|
||||
else:
|
||||
# This additional assignment was needed to prevent a mypy error.
|
||||
ssl = _ssl
|
||||
|
||||
try:
|
||||
import ipaddress
|
||||
except ImportError:
|
||||
@@ -41,16 +32,13 @@ except ImportError:
|
||||
|
||||
|
||||
__all__ = [
|
||||
"ipaddress", "uses_pycache", "console_to_str", "native_str",
|
||||
"ipaddress", "uses_pycache", "console_to_str",
|
||||
"get_path_uid", "stdlib_pkgs", "WINDOWS", "samefile", "get_terminal_size",
|
||||
"get_extension_suffixes",
|
||||
]
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
HAS_TLS = (ssl is not None) or IS_PYOPENSSL
|
||||
|
||||
if PY2:
|
||||
import imp
|
||||
|
||||
@@ -86,6 +74,18 @@ else:
|
||||
backslashreplace_decode = "backslashreplace"
|
||||
|
||||
|
||||
def has_tls():
|
||||
# type: () -> bool
|
||||
try:
|
||||
import _ssl # noqa: F401 # ignore unused
|
||||
return True
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
from pipenv.patched.notpip._vendor.urllib3.util import IS_PYOPENSSL
|
||||
return IS_PYOPENSSL
|
||||
|
||||
|
||||
def str_to_display(data, desc=None):
|
||||
# type: (Union[bytes, Text], Optional[str]) -> Text
|
||||
"""
|
||||
@@ -159,22 +159,6 @@ def console_to_str(data):
|
||||
return str_to_display(data, desc='Subprocess output')
|
||||
|
||||
|
||||
if PY2:
|
||||
def native_str(s, replace=False):
|
||||
# type: (str, bool) -> str
|
||||
# Replace is ignored -- unicode to UTF-8 can't fail
|
||||
if isinstance(s, text_type):
|
||||
return s.encode('utf-8')
|
||||
return s
|
||||
|
||||
else:
|
||||
def native_str(s, replace=False):
|
||||
# type: (str, bool) -> str
|
||||
if isinstance(s, bytes):
|
||||
return s.decode('utf-8', 'replace' if replace else 'strict')
|
||||
return s
|
||||
|
||||
|
||||
def get_path_uid(path):
|
||||
# type: (str) -> int
|
||||
"""
|
||||
@@ -205,19 +189,6 @@ def get_path_uid(path):
|
||||
return file_uid
|
||||
|
||||
|
||||
if PY2:
|
||||
from imp import get_suffixes
|
||||
|
||||
def get_extension_suffixes():
|
||||
return [suffix[0] for suffix in get_suffixes()]
|
||||
|
||||
else:
|
||||
from importlib.machinery import EXTENSION_SUFFIXES
|
||||
|
||||
def get_extension_suffixes():
|
||||
return EXTENSION_SUFFIXES
|
||||
|
||||
|
||||
def expanduser(path):
|
||||
# type: (str) -> str
|
||||
"""
|
||||
@@ -286,12 +257,13 @@ else:
|
||||
return cr
|
||||
cr = ioctl_GWINSZ(0) or ioctl_GWINSZ(1) or ioctl_GWINSZ(2)
|
||||
if not cr:
|
||||
try:
|
||||
fd = os.open(os.ctermid(), os.O_RDONLY)
|
||||
cr = ioctl_GWINSZ(fd)
|
||||
os.close(fd)
|
||||
except Exception:
|
||||
pass
|
||||
if sys.platform != "win32":
|
||||
try:
|
||||
fd = os.open(os.ctermid(), os.O_RDONLY)
|
||||
cr = ioctl_GWINSZ(fd)
|
||||
os.close(fd)
|
||||
except Exception:
|
||||
pass
|
||||
if not cr:
|
||||
cr = (os.environ.get('LINES', 25), os.environ.get('COLUMNS', 80))
|
||||
return int(cr[1]), int(cr[0])
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
from distutils.errors import DistutilsArgError
|
||||
from distutils.fancy_getopt import FancyGetopt
|
||||
|
||||
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
|
||||
|
||||
if MYPY_CHECK_RUNNING:
|
||||
from typing import Dict, List
|
||||
|
||||
|
||||
_options = [
|
||||
("exec-prefix=", None, ""),
|
||||
("home=", None, ""),
|
||||
("install-base=", None, ""),
|
||||
("install-data=", None, ""),
|
||||
("install-headers=", None, ""),
|
||||
("install-lib=", None, ""),
|
||||
("install-platlib=", None, ""),
|
||||
("install-purelib=", None, ""),
|
||||
("install-scripts=", None, ""),
|
||||
("prefix=", None, ""),
|
||||
("root=", None, ""),
|
||||
("user", None, ""),
|
||||
]
|
||||
|
||||
|
||||
# typeshed doesn't permit Tuple[str, None, str], see python/typeshed#3469.
|
||||
_distutils_getopt = FancyGetopt(_options) # type: ignore
|
||||
|
||||
|
||||
def parse_distutils_args(args):
|
||||
# type: (List[str]) -> Dict[str, str]
|
||||
"""Parse provided arguments, returning an object that has the
|
||||
matched arguments.
|
||||
|
||||
Any unknown arguments are ignored.
|
||||
"""
|
||||
result = {}
|
||||
for arg in args:
|
||||
try:
|
||||
_, match = _distutils_getopt.getopt(args=[arg])
|
||||
except DistutilsArgError:
|
||||
# We don't care about any other options, which here may be
|
||||
# considered unrecognized since our option list is not
|
||||
# exhaustive.
|
||||
pass
|
||||
else:
|
||||
result.update(match.__dict__)
|
||||
return result
|
||||
@@ -0,0 +1,31 @@
|
||||
import sys
|
||||
|
||||
from pipenv.patched.notpip._internal.cli.main import main
|
||||
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
|
||||
|
||||
if MYPY_CHECK_RUNNING:
|
||||
from typing import Optional, List
|
||||
|
||||
|
||||
def _wrapper(args=None):
|
||||
# type: (Optional[List[str]]) -> int
|
||||
"""Central wrapper for all old entrypoints.
|
||||
|
||||
Historically pip has had several entrypoints defined. Because of issues
|
||||
arising from PATH, sys.path, multiple Pythons, their interactions, and most
|
||||
of them having a pip installed, users suffer every time an entrypoint gets
|
||||
moved.
|
||||
|
||||
To alleviate this pain, and provide a mechanism for warning users and
|
||||
directing them to an appropriate place for help, we now define all of
|
||||
our old entrypoints as wrappers for the current one.
|
||||
"""
|
||||
sys.stderr.write(
|
||||
"WARNING: pip is being invoked by an old script wrapper. This will "
|
||||
"fail in a future version of pip.\n"
|
||||
"Please see https://github.com/pypa/pip/issues/5599 for advice on "
|
||||
"fixing the underlying issue.\n"
|
||||
"To avoid this problem you can invoke Python with '-m pip' instead of "
|
||||
"running pip directly.\n"
|
||||
)
|
||||
return main(args)
|
||||
@@ -1,7 +1,10 @@
|
||||
import errno
|
||||
import os
|
||||
import os.path
|
||||
import random
|
||||
import shutil
|
||||
import stat
|
||||
import sys
|
||||
from contextlib import contextmanager
|
||||
from tempfile import NamedTemporaryFile
|
||||
|
||||
@@ -11,8 +14,7 @@ from pipenv.patched.notpip._vendor.retrying import retry # type: ignore
|
||||
from pipenv.patched.notpip._vendor.six import PY2
|
||||
|
||||
from pipenv.patched.notpip._internal.utils.compat import get_path_uid
|
||||
from pipenv.patched.notpip._internal.utils.misc import cast
|
||||
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
|
||||
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING, cast
|
||||
|
||||
if MYPY_CHECK_RUNNING:
|
||||
from typing import BinaryIO, Iterator
|
||||
@@ -28,9 +30,11 @@ def check_path_owner(path):
|
||||
# type: (str) -> bool
|
||||
# If we don't have a way to check the effective uid of this process, then
|
||||
# we'll just assume that we own the directory.
|
||||
if not hasattr(os, "geteuid"):
|
||||
if sys.platform == "win32" or not hasattr(os, "geteuid"):
|
||||
return True
|
||||
|
||||
assert os.path.isabs(path)
|
||||
|
||||
previous = None
|
||||
while path != previous:
|
||||
if os.path.lexists(path):
|
||||
@@ -113,3 +117,55 @@ if PY2:
|
||||
|
||||
else:
|
||||
replace = _replace_retry(os.replace)
|
||||
|
||||
|
||||
# test_writable_dir and _test_writable_dir_win are copied from Flit,
|
||||
# with the author's agreement to also place them under pip's license.
|
||||
def test_writable_dir(path):
|
||||
# type: (str) -> bool
|
||||
"""Check if a directory is writable.
|
||||
|
||||
Uses os.access() on POSIX, tries creating files on Windows.
|
||||
"""
|
||||
# If the directory doesn't exist, find the closest parent that does.
|
||||
while not os.path.isdir(path):
|
||||
parent = os.path.dirname(path)
|
||||
if parent == path:
|
||||
break # Should never get here, but infinite loops are bad
|
||||
path = parent
|
||||
|
||||
if os.name == 'posix':
|
||||
return os.access(path, os.W_OK)
|
||||
|
||||
return _test_writable_dir_win(path)
|
||||
|
||||
|
||||
def _test_writable_dir_win(path):
|
||||
# type: (str) -> bool
|
||||
# os.access doesn't work on Windows: http://bugs.python.org/issue2528
|
||||
# and we can't use tempfile: http://bugs.python.org/issue22107
|
||||
basename = 'accesstest_deleteme_fishfingers_custard_'
|
||||
alphabet = 'abcdefghijklmnopqrstuvwxyz0123456789'
|
||||
for i in range(10):
|
||||
name = basename + ''.join(random.choice(alphabet) for _ in range(6))
|
||||
file = os.path.join(path, name)
|
||||
try:
|
||||
fd = os.open(file, os.O_RDWR | os.O_CREAT | os.O_EXCL)
|
||||
except OSError as e:
|
||||
if e.errno == errno.EEXIST:
|
||||
continue
|
||||
if e.errno == errno.EPERM:
|
||||
# This could be because there's a directory with the same name.
|
||||
# But it's highly unlikely there's a directory called that,
|
||||
# so we'll assume it's because the parent dir is not writable.
|
||||
return False
|
||||
raise
|
||||
else:
|
||||
os.close(fd)
|
||||
os.unlink(file)
|
||||
return True
|
||||
|
||||
# This should never be reached
|
||||
raise EnvironmentError(
|
||||
'Unexpected condition testing for writable directory'
|
||||
)
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
from __future__ import absolute_import
|
||||
|
||||
import os
|
||||
import re
|
||||
import warnings
|
||||
import sys
|
||||
|
||||
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
|
||||
|
||||
@@ -26,6 +25,8 @@ def glibc_version_string_confstr():
|
||||
# to be broken or missing. This strategy is used in the standard library
|
||||
# platform module:
|
||||
# https://github.com/python/cpython/blob/fcf1d003bf4f0100c9d0921ff3d70e1127ca1b71/Lib/platform.py#L175-L183
|
||||
if sys.platform == "win32":
|
||||
return None
|
||||
try:
|
||||
# os.confstr("CS_GNU_LIBC_VERSION") returns a string like "glibc 2.17":
|
||||
_, version = os.confstr("CS_GNU_LIBC_VERSION").split()
|
||||
@@ -66,32 +67,6 @@ def glibc_version_string_ctypes():
|
||||
return version_str
|
||||
|
||||
|
||||
# Separated out from have_compatible_glibc for easier unit testing
|
||||
def check_glibc_version(version_str, required_major, minimum_minor):
|
||||
# type: (str, int, int) -> bool
|
||||
# Parse string and check against requested version.
|
||||
#
|
||||
# We use a regexp instead of str.split because we want to discard any
|
||||
# random junk that might come after the minor version -- this might happen
|
||||
# in patched/forked versions of glibc (e.g. Linaro's version of glibc
|
||||
# uses version strings like "2.20-2014.11"). See gh-3588.
|
||||
m = re.match(r"(?P<major>[0-9]+)\.(?P<minor>[0-9]+)", version_str)
|
||||
if not m:
|
||||
warnings.warn("Expected glibc version with 2 components major.minor,"
|
||||
" got: %s" % version_str, RuntimeWarning)
|
||||
return False
|
||||
return (int(m.group("major")) == required_major and
|
||||
int(m.group("minor")) >= minimum_minor)
|
||||
|
||||
|
||||
def have_compatible_glibc(required_major, minimum_minor):
|
||||
# type: (int, int) -> bool
|
||||
version_str = glibc_version_string()
|
||||
if version_str is None:
|
||||
return False
|
||||
return check_glibc_version(version_str, required_major, minimum_minor)
|
||||
|
||||
|
||||
# platform.libc_ver regularly returns completely nonsensical glibc
|
||||
# versions. E.g. on my computer, platform says:
|
||||
#
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
# The following comment should be removed at some point in the future.
|
||||
# mypy: disallow-untyped-defs=False
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import hashlib
|
||||
@@ -59,6 +56,7 @@ class Hashes(object):
|
||||
hash_name, # type: str
|
||||
hex_digest, # type: str
|
||||
):
|
||||
# type: (...) -> bool
|
||||
"""Return whether the given hex digest is allowed."""
|
||||
return hex_digest in self._allowed.get(hash_name, [])
|
||||
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
# The following comment should be removed at some point in the future.
|
||||
# mypy: disallow-untyped-defs=False
|
||||
|
||||
import os.path
|
||||
|
||||
DELETE_MARKER_MESSAGE = '''\
|
||||
@@ -14,6 +11,7 @@ PIP_DELETE_MARKER_FILENAME = 'pip-delete-this-directory.txt'
|
||||
|
||||
|
||||
def has_delete_marker_file(directory):
|
||||
# type: (str) -> bool
|
||||
return os.path.exists(os.path.join(directory, PIP_DELETE_MARKER_FILENAME))
|
||||
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ from __future__ import absolute_import
|
||||
import contextlib
|
||||
import errno
|
||||
import getpass
|
||||
import hashlib
|
||||
import io
|
||||
import logging
|
||||
import os
|
||||
@@ -38,8 +39,7 @@ from pipenv.patched.notpip._internal.utils.compat import (
|
||||
stdlib_pkgs,
|
||||
str_to_display,
|
||||
)
|
||||
from pipenv.patched.notpip._internal.utils.marker_files import write_delete_marker_file
|
||||
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
|
||||
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING, cast
|
||||
from pipenv.patched.notpip._internal.utils.virtualenv import (
|
||||
running_under_virtualenv,
|
||||
virtualenv_no_global,
|
||||
@@ -53,16 +53,11 @@ else:
|
||||
if MYPY_CHECK_RUNNING:
|
||||
from typing import (
|
||||
Any, AnyStr, Container, Iterable, List, Optional, Text,
|
||||
Tuple, Union, cast,
|
||||
Tuple, Union,
|
||||
)
|
||||
from pipenv.patched.notpip._vendor.pkg_resources import Distribution
|
||||
|
||||
VersionInfo = Tuple[int, int, int]
|
||||
else:
|
||||
# typing's cast() is needed at runtime, but we don't want to import typing.
|
||||
# Thus, we use a dummy no-op version, which we tell mypy to ignore.
|
||||
def cast(type_, value): # type: ignore
|
||||
return value
|
||||
|
||||
|
||||
__all__ = ['rmtree', 'display_path', 'backup_dir',
|
||||
@@ -115,7 +110,8 @@ def ensure_dir(path):
|
||||
try:
|
||||
os.makedirs(path)
|
||||
except OSError as e:
|
||||
if e.errno != errno.EEXIST:
|
||||
# Windows can raise spurious ENOTEMPTY errors. See #6426.
|
||||
if e.errno != errno.EEXIST and e.errno != errno.ENOTEMPTY:
|
||||
raise
|
||||
|
||||
|
||||
@@ -270,13 +266,13 @@ def ask_password(message):
|
||||
def format_size(bytes):
|
||||
# type: (float) -> str
|
||||
if bytes > 1000 * 1000:
|
||||
return '%.1fMB' % (bytes / 1000.0 / 1000)
|
||||
return '%.1f MB' % (bytes / 1000.0 / 1000)
|
||||
elif bytes > 10 * 1000:
|
||||
return '%ikB' % (bytes / 1000)
|
||||
return '%i kB' % (bytes / 1000)
|
||||
elif bytes > 1000:
|
||||
return '%.1fkB' % (bytes / 1000.0)
|
||||
return '%.1f kB' % (bytes / 1000.0)
|
||||
else:
|
||||
return '%ibytes' % bytes
|
||||
return '%i bytes' % bytes
|
||||
|
||||
|
||||
def is_installable_dir(path):
|
||||
@@ -460,8 +456,7 @@ def get_installed_distributions(
|
||||
def user_test(d):
|
||||
return True
|
||||
|
||||
# because of pkg_resources vendoring, mypy cannot find stub in typeshed
|
||||
return [d for d in working_set # type: ignore
|
||||
return [d for d in working_set
|
||||
if local_test(d) and
|
||||
d.key not in skip and
|
||||
editable_test(d) and
|
||||
@@ -527,11 +522,6 @@ def write_output(msg, *args):
|
||||
logger.info(msg, *args)
|
||||
|
||||
|
||||
def _make_build_dir(build_dir):
|
||||
os.makedirs(build_dir)
|
||||
write_delete_marker_file(build_dir)
|
||||
|
||||
|
||||
class FakeFile(object):
|
||||
"""Wrap a list of lines in an object with readline() to make
|
||||
ConfigParser happy."""
|
||||
@@ -840,11 +830,11 @@ def protect_pip_from_modification_on_windows(modifying_pip):
|
||||
On Windows, any operation modifying pip should be run as:
|
||||
python -m pip ...
|
||||
"""
|
||||
pip_names = set()
|
||||
for ext in ('', '.exe'):
|
||||
pip_names.add('pip{ext}'.format(ext=ext))
|
||||
pip_names.add('pip{}{ext}'.format(sys.version_info[0], ext=ext))
|
||||
pip_names.add('pip{}.{}{ext}'.format(*sys.version_info[:2], ext=ext))
|
||||
pip_names = [
|
||||
"pip.exe",
|
||||
"pip{}.exe".format(sys.version_info[0]),
|
||||
"pip{}.{}.exe".format(*sys.version_info[:2])
|
||||
]
|
||||
|
||||
# See https://github.com/pypa/pip/issues/1299 for more discussion
|
||||
should_show_use_python_msg = (
|
||||
@@ -868,3 +858,29 @@ def is_console_interactive():
|
||||
"""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]
|
||||
"""Return (hash, length) for path using hashlib.sha256()
|
||||
"""
|
||||
|
||||
h = hashlib.sha256()
|
||||
length = 0
|
||||
with open(path, 'rb') as f:
|
||||
for block in read_chunks(f, size=blocksize):
|
||||
length += len(block)
|
||||
h.update(block)
|
||||
return h, length
|
||||
|
||||
|
||||
def is_wheel_installed():
|
||||
"""
|
||||
Return whether the wheel package is installed.
|
||||
"""
|
||||
try:
|
||||
import wheel # noqa: F401
|
||||
except ImportError:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
from pipenv.patched.notpip._vendor.pkg_resources import yield_lines
|
||||
from pipenv.patched.notpip._vendor.six import ensure_str
|
||||
|
||||
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
|
||||
|
||||
if MYPY_CHECK_RUNNING:
|
||||
from typing import Dict, Iterable, List
|
||||
|
||||
|
||||
class DictMetadata(object):
|
||||
"""IMetadataProvider that reads metadata files from a dictionary.
|
||||
"""
|
||||
def __init__(self, metadata):
|
||||
# type: (Dict[str, bytes]) -> None
|
||||
self._metadata = metadata
|
||||
|
||||
def has_metadata(self, name):
|
||||
# type: (str) -> bool
|
||||
return name in self._metadata
|
||||
|
||||
def get_metadata(self, name):
|
||||
# type: (str) -> str
|
||||
try:
|
||||
return ensure_str(self._metadata[name])
|
||||
except UnicodeDecodeError as e:
|
||||
# Mirrors handling done in pkg_resources.NullProvider.
|
||||
e.reason += " in {} file".format(name)
|
||||
raise
|
||||
|
||||
def get_metadata_lines(self, name):
|
||||
# type: (str) -> Iterable[str]
|
||||
return yield_lines(self.get_metadata(name))
|
||||
|
||||
def metadata_isdir(self, name):
|
||||
# type: (str) -> bool
|
||||
return False
|
||||
|
||||
def metadata_listdir(self, name):
|
||||
# type: (str) -> List[str]
|
||||
return []
|
||||
|
||||
def run_script(self, script_name, namespace):
|
||||
# type: (str, str) -> None
|
||||
pass
|
||||
@@ -4,7 +4,7 @@ import sys
|
||||
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
|
||||
|
||||
if MYPY_CHECK_RUNNING:
|
||||
from typing import List, Sequence
|
||||
from typing import List, Optional, Sequence
|
||||
|
||||
# Shim to wrap setup.py invocation with setuptools
|
||||
#
|
||||
@@ -22,10 +22,10 @@ _SETUPTOOLS_SHIM = (
|
||||
|
||||
|
||||
def make_setuptools_shim_args(
|
||||
setup_py_path, # type: str
|
||||
global_options=None, # type: Sequence[str]
|
||||
no_user_config=False, # type: bool
|
||||
unbuffered_output=False # type: bool
|
||||
setup_py_path, # type: str
|
||||
global_options=None, # type: Sequence[str]
|
||||
no_user_config=False, # type: bool
|
||||
unbuffered_output=False # type: bool
|
||||
):
|
||||
# type: (...) -> List[str]
|
||||
"""
|
||||
@@ -40,10 +40,144 @@ def make_setuptools_shim_args(
|
||||
sys_executable = os.environ.get('PIP_PYTHON_PATH', sys.executable)
|
||||
args = [sys_executable]
|
||||
if unbuffered_output:
|
||||
args.append('-u')
|
||||
args.extend(['-c', _SETUPTOOLS_SHIM.format(setup_py_path)])
|
||||
args += ["-u"]
|
||||
args += ["-c", _SETUPTOOLS_SHIM.format(setup_py_path)]
|
||||
if global_options:
|
||||
args.extend(global_options)
|
||||
args += global_options
|
||||
if no_user_config:
|
||||
args.append('--no-user-cfg')
|
||||
args += ["--no-user-cfg"]
|
||||
return args
|
||||
|
||||
|
||||
def make_setuptools_bdist_wheel_args(
|
||||
setup_py_path, # type: str
|
||||
global_options, # type: Sequence[str]
|
||||
build_options, # type: Sequence[str]
|
||||
destination_dir, # type: str
|
||||
):
|
||||
# type: (...) -> List[str]
|
||||
# NOTE: Eventually, we'd want to also -S to the flags here, when we're
|
||||
# isolating. Currently, it breaks Python in virtualenvs, because it
|
||||
# relies on site.py to find parts of the standard library outside the
|
||||
# virtualenv.
|
||||
args = make_setuptools_shim_args(
|
||||
setup_py_path,
|
||||
global_options=global_options,
|
||||
unbuffered_output=True
|
||||
)
|
||||
args += ["bdist_wheel", "-d", destination_dir]
|
||||
args += build_options
|
||||
return args
|
||||
|
||||
|
||||
def make_setuptools_clean_args(
|
||||
setup_py_path, # type: str
|
||||
global_options, # type: Sequence[str]
|
||||
):
|
||||
# type: (...) -> List[str]
|
||||
args = make_setuptools_shim_args(
|
||||
setup_py_path,
|
||||
global_options=global_options,
|
||||
unbuffered_output=True
|
||||
)
|
||||
args += ["clean", "--all"]
|
||||
return args
|
||||
|
||||
|
||||
def make_setuptools_develop_args(
|
||||
setup_py_path, # type: str
|
||||
global_options, # type: Sequence[str]
|
||||
install_options, # type: Sequence[str]
|
||||
no_user_config, # type: bool
|
||||
prefix, # type: Optional[str]
|
||||
home, # type: Optional[str]
|
||||
use_user_site, # type: bool
|
||||
):
|
||||
# type: (...) -> List[str]
|
||||
assert not (use_user_site and prefix)
|
||||
|
||||
args = make_setuptools_shim_args(
|
||||
setup_py_path,
|
||||
global_options=global_options,
|
||||
no_user_config=no_user_config,
|
||||
)
|
||||
|
||||
args += ["develop", "--no-deps"]
|
||||
|
||||
args += install_options
|
||||
|
||||
if prefix:
|
||||
args += ["--prefix", prefix]
|
||||
if home is not None:
|
||||
args += ["--home", home]
|
||||
|
||||
if use_user_site:
|
||||
args += ["--user", "--prefix="]
|
||||
|
||||
return args
|
||||
|
||||
|
||||
def make_setuptools_egg_info_args(
|
||||
setup_py_path, # type: str
|
||||
egg_info_dir, # type: Optional[str]
|
||||
no_user_config, # type: bool
|
||||
):
|
||||
# type: (...) -> List[str]
|
||||
args = make_setuptools_shim_args(setup_py_path)
|
||||
if no_user_config:
|
||||
args += ["--no-user-cfg"]
|
||||
|
||||
args += ["egg_info"]
|
||||
|
||||
if egg_info_dir:
|
||||
args += ["--egg-base", egg_info_dir]
|
||||
|
||||
return args
|
||||
|
||||
|
||||
def make_setuptools_install_args(
|
||||
setup_py_path, # type: str
|
||||
global_options, # type: Sequence[str]
|
||||
install_options, # type: Sequence[str]
|
||||
record_filename, # type: str
|
||||
root, # type: Optional[str]
|
||||
prefix, # type: Optional[str]
|
||||
header_dir, # type: Optional[str]
|
||||
home, # type: Optional[str]
|
||||
use_user_site, # type: bool
|
||||
no_user_config, # type: bool
|
||||
pycompile # type: bool
|
||||
):
|
||||
# type: (...) -> List[str]
|
||||
assert not (use_user_site and prefix)
|
||||
assert not (use_user_site and root)
|
||||
|
||||
args = make_setuptools_shim_args(
|
||||
setup_py_path,
|
||||
global_options=global_options,
|
||||
no_user_config=no_user_config,
|
||||
unbuffered_output=True
|
||||
)
|
||||
args += ["install", "--record", record_filename]
|
||||
args += ["--single-version-externally-managed"]
|
||||
|
||||
if root is not None:
|
||||
args += ["--root", root]
|
||||
if prefix is not None:
|
||||
args += ["--prefix", prefix]
|
||||
if home is not None:
|
||||
args += ["--home", home]
|
||||
if use_user_site:
|
||||
args += ["--user", "--prefix="]
|
||||
|
||||
if pycompile:
|
||||
args += ["--compile"]
|
||||
else:
|
||||
args += ["--no-compile"]
|
||||
|
||||
if header_dir:
|
||||
args += ["--install-headers", header_dir]
|
||||
|
||||
args += install_options
|
||||
|
||||
return args
|
||||
|
||||
@@ -254,7 +254,7 @@ def call_subprocess(
|
||||
|
||||
|
||||
def runner_with_spinner_message(message):
|
||||
# type: (str) -> Callable
|
||||
# type: (str) -> Callable[..., None]
|
||||
"""Provide a subprocess_runner that shows a spinner message.
|
||||
|
||||
Intended for use with for pep517's Pep517HookCaller. Thus, the runner has
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
# The following comment should be removed at some point in the future.
|
||||
# mypy: disallow-untyped-defs=False
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import errno
|
||||
@@ -8,6 +5,9 @@ import itertools
|
||||
import logging
|
||||
import os.path
|
||||
import tempfile
|
||||
from contextlib import contextmanager
|
||||
|
||||
from pipenv.patched.notpip._vendor.contextlib2 import ExitStack
|
||||
import warnings
|
||||
|
||||
from pipenv.patched.notpip._internal.utils.misc import rmtree
|
||||
@@ -15,12 +15,70 @@ from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
|
||||
from pipenv.vendor.vistir.compat import finalize, ResourceWarning
|
||||
|
||||
if MYPY_CHECK_RUNNING:
|
||||
from typing import Optional
|
||||
from typing import Any, Dict, Iterator, Optional, TypeVar
|
||||
|
||||
_T = TypeVar('_T', bound='TempDirectory')
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
_tempdir_manager = None # type: Optional[ExitStack]
|
||||
|
||||
|
||||
@contextmanager
|
||||
def global_tempdir_manager():
|
||||
# type: () -> Iterator[None]
|
||||
global _tempdir_manager
|
||||
with ExitStack() as stack:
|
||||
old_tempdir_manager, _tempdir_manager = _tempdir_manager, stack
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
_tempdir_manager = old_tempdir_manager
|
||||
|
||||
|
||||
class TempDirectoryTypeRegistry(object):
|
||||
"""Manages temp directory behavior
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
# type: () -> None
|
||||
self._should_delete = {} # type: Dict[str, bool]
|
||||
|
||||
def set_delete(self, kind, value):
|
||||
# type: (str, bool) -> None
|
||||
"""Indicate whether a TempDirectory of the given kind should be
|
||||
auto-deleted.
|
||||
"""
|
||||
self._should_delete[kind] = value
|
||||
|
||||
def get_delete(self, kind):
|
||||
# type: (str) -> bool
|
||||
"""Get configured auto-delete flag for a given TempDirectory type,
|
||||
default True.
|
||||
"""
|
||||
return self._should_delete.get(kind, True)
|
||||
|
||||
|
||||
_tempdir_registry = None # type: Optional[TempDirectoryTypeRegistry]
|
||||
|
||||
|
||||
@contextmanager
|
||||
def tempdir_registry():
|
||||
# type: () -> Iterator[TempDirectoryTypeRegistry]
|
||||
"""Provides a scoped global tempdir registry that can be used to dictate
|
||||
whether directories should be deleted.
|
||||
"""
|
||||
global _tempdir_registry
|
||||
old_tempdir_registry = _tempdir_registry
|
||||
_tempdir_registry = TempDirectoryTypeRegistry()
|
||||
try:
|
||||
yield _tempdir_registry
|
||||
finally:
|
||||
_tempdir_registry = old_tempdir_registry
|
||||
|
||||
|
||||
class TempDirectory(object):
|
||||
"""Helper class that owns and cleans up a temporary directory.
|
||||
|
||||
@@ -46,14 +104,15 @@ class TempDirectory(object):
|
||||
self,
|
||||
path=None, # type: Optional[str]
|
||||
delete=None, # type: Optional[bool]
|
||||
kind="temp"
|
||||
kind="temp", # type: str
|
||||
globally_managed=False, # type: bool
|
||||
):
|
||||
super(TempDirectory, self).__init__()
|
||||
|
||||
if path is None and delete is None:
|
||||
# If we were not given an explicit directory, and we were not given
|
||||
# an explicit delete option, then we'll default to deleting.
|
||||
delete = True
|
||||
# If we were given an explicit directory, resolve delete option now.
|
||||
# Otherwise we wait until cleanup and see what tempdir_registry says.
|
||||
if path is not None and delete is None:
|
||||
delete = False
|
||||
|
||||
if path is None:
|
||||
path = self._create(kind)
|
||||
@@ -66,6 +125,10 @@ class TempDirectory(object):
|
||||
if self._path:
|
||||
self._register_finalizer()
|
||||
|
||||
if globally_managed:
|
||||
assert _tempdir_manager is not None
|
||||
_tempdir_manager.enter_context(self)
|
||||
|
||||
def _register_finalizer(self):
|
||||
if self.delete and self._path:
|
||||
self._finalizer = finalize(
|
||||
@@ -86,16 +149,27 @@ class TempDirectory(object):
|
||||
return self._path
|
||||
|
||||
def __repr__(self):
|
||||
# type: () -> str
|
||||
return "<{} {!r}>".format(self.__class__.__name__, self.path)
|
||||
|
||||
def __enter__(self):
|
||||
# type: (_T) -> _T
|
||||
return self
|
||||
|
||||
def __exit__(self, exc, value, tb):
|
||||
if self.delete:
|
||||
# type: (Any, Any, Any) -> None
|
||||
if self.delete is not None:
|
||||
delete = self.delete
|
||||
elif _tempdir_registry:
|
||||
delete = _tempdir_registry.get_delete(self.kind)
|
||||
else:
|
||||
delete = True
|
||||
|
||||
if delete:
|
||||
self.cleanup()
|
||||
|
||||
def _create(self, kind):
|
||||
# type: (str) -> str
|
||||
"""Create a temporary directory and store its path in self.path
|
||||
"""
|
||||
# We realpath here because some systems have their default tmpdir
|
||||
@@ -121,6 +195,7 @@ class TempDirectory(object):
|
||||
warnings.warn(warn_message, ResourceWarning)
|
||||
|
||||
def cleanup(self):
|
||||
# type: () -> None
|
||||
"""Remove the temporary directory created and reset state
|
||||
"""
|
||||
if getattr(self._finalizer, "detach", None) and self._finalizer.detach():
|
||||
@@ -154,11 +229,13 @@ class AdjacentTempDirectory(TempDirectory):
|
||||
LEADING_CHARS = "-~.=%0123456789"
|
||||
|
||||
def __init__(self, original, delete=None):
|
||||
# type: (str, Optional[bool]) -> None
|
||||
self.original = original.rstrip('/\\')
|
||||
super(AdjacentTempDirectory, self).__init__(delete=delete)
|
||||
|
||||
@classmethod
|
||||
def _generate_names(cls, name):
|
||||
# type: (str) -> Iterator[str]
|
||||
"""Generates a series of temporary names.
|
||||
|
||||
The algorithm replaces the leading characters in the name
|
||||
@@ -182,6 +259,7 @@ class AdjacentTempDirectory(TempDirectory):
|
||||
yield new_name
|
||||
|
||||
def _create(self, kind):
|
||||
# type: (str) -> str
|
||||
root, name = os.path.split(self.original)
|
||||
for candidate in self._generate_names(name):
|
||||
path = os.path.join(root, candidate)
|
||||
@@ -201,4 +279,4 @@ class AdjacentTempDirectory(TempDirectory):
|
||||
)
|
||||
|
||||
logger.debug("Created temporary directory: {}".format(path))
|
||||
return path
|
||||
return path
|
||||
|
||||
@@ -27,3 +27,12 @@ Ref: https://github.com/python/mypy/issues/3216
|
||||
"""
|
||||
|
||||
MYPY_CHECK_RUNNING = False
|
||||
|
||||
|
||||
if MYPY_CHECK_RUNNING:
|
||||
from typing import cast
|
||||
else:
|
||||
# typing's cast() is needed at runtime, but we don't want to import typing.
|
||||
# Thus, we use a dummy no-op version, which we tell mypy to ignore.
|
||||
def cast(type_, value): # type: ignore
|
||||
return value
|
||||
|
||||
@@ -156,10 +156,10 @@ class DownloadProgressMixin(object):
|
||||
return "eta %s" % self.eta_td
|
||||
return ""
|
||||
|
||||
def iter(self, it, n=1):
|
||||
def iter(self, it):
|
||||
for x in it:
|
||||
yield x
|
||||
self.next(n)
|
||||
self.next(len(x))
|
||||
self.finish()
|
||||
|
||||
|
||||
@@ -279,7 +279,7 @@ def DownloadProgressProvider(progress_bar, max=None):
|
||||
|
||||
@contextlib.contextmanager
|
||||
def hidden_cursor(file):
|
||||
# type: (IO) -> Iterator[None]
|
||||
# type: (IO[Any]) -> Iterator[None]
|
||||
# The Windows terminal does not support the hide/show cursor ANSI codes,
|
||||
# even via colorama. So don't even try.
|
||||
if WINDOWS:
|
||||
|
||||
@@ -1,34 +1,115 @@
|
||||
import os.path
|
||||
from __future__ import absolute_import
|
||||
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import site
|
||||
import sys
|
||||
|
||||
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
|
||||
|
||||
if MYPY_CHECK_RUNNING:
|
||||
from typing import List, Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
_INCLUDE_SYSTEM_SITE_PACKAGES_REGEX = re.compile(
|
||||
r"include-system-site-packages\s*=\s*(?P<value>true|false)"
|
||||
)
|
||||
|
||||
|
||||
def _running_under_venv():
|
||||
# type: () -> bool
|
||||
"""Checks if sys.base_prefix and sys.prefix match.
|
||||
|
||||
This handles PEP 405 compliant virtual environments.
|
||||
"""
|
||||
return sys.prefix != getattr(sys, "base_prefix", sys.prefix)
|
||||
|
||||
|
||||
def _running_under_regular_virtualenv():
|
||||
# type: () -> bool
|
||||
"""Checks if sys.real_prefix is set.
|
||||
|
||||
This handles virtual environments created with pypa's virtualenv.
|
||||
"""
|
||||
# pypa/virtualenv case
|
||||
return hasattr(sys, 'real_prefix')
|
||||
|
||||
|
||||
def running_under_virtualenv():
|
||||
# type: () -> bool
|
||||
"""Return True if we're running inside a virtualenv, False otherwise.
|
||||
"""
|
||||
Return True if we're running inside a virtualenv, False otherwise.
|
||||
return _running_under_venv() or _running_under_regular_virtualenv()
|
||||
|
||||
|
||||
def _get_pyvenv_cfg_lines():
|
||||
# type: () -> Optional[List[str]]
|
||||
"""Reads {sys.prefix}/pyvenv.cfg and returns its contents as list of lines
|
||||
|
||||
Returns None, if it could not read/access the file.
|
||||
"""
|
||||
if hasattr(sys, 'real_prefix'):
|
||||
# pypa/virtualenv case
|
||||
return True
|
||||
elif sys.prefix != getattr(sys, "base_prefix", sys.prefix):
|
||||
# PEP 405 venv
|
||||
pyvenv_cfg_file = os.path.join(sys.prefix, 'pyvenv.cfg')
|
||||
try:
|
||||
with open(pyvenv_cfg_file) as f:
|
||||
return f.read().splitlines() # avoids trailing newlines
|
||||
except IOError:
|
||||
return None
|
||||
|
||||
|
||||
def _no_global_under_venv():
|
||||
# type: () -> bool
|
||||
"""Check `{sys.prefix}/pyvenv.cfg` for system site-packages inclusion
|
||||
|
||||
PEP 405 specifies that when system site-packages are not supposed to be
|
||||
visible from a virtual environment, `pyvenv.cfg` must contain the following
|
||||
line:
|
||||
|
||||
include-system-site-packages = false
|
||||
|
||||
Additionally, log a warning if accessing the file fails.
|
||||
"""
|
||||
cfg_lines = _get_pyvenv_cfg_lines()
|
||||
if cfg_lines is None:
|
||||
# We're not in a "sane" venv, so assume there is no system
|
||||
# site-packages access (since that's PEP 405's default state).
|
||||
logger.warning(
|
||||
"Could not access 'pyvenv.cfg' despite a virtual environment "
|
||||
"being active. Assuming global site-packages is not accessible "
|
||||
"in this environment."
|
||||
)
|
||||
return True
|
||||
|
||||
for line in cfg_lines:
|
||||
match = _INCLUDE_SYSTEM_SITE_PACKAGES_REGEX.match(line)
|
||||
if match is not None and match.group('value') == 'false':
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _no_global_under_regular_virtualenv():
|
||||
# type: () -> bool
|
||||
"""Check if "no-global-site-packages.txt" exists beside site.py
|
||||
|
||||
This mirrors logic in pypa/virtualenv for determining whether system
|
||||
site-packages are visible in the virtual environment.
|
||||
"""
|
||||
site_mod_dir = os.path.dirname(os.path.abspath(site.__file__))
|
||||
no_global_site_packages_file = os.path.join(
|
||||
site_mod_dir, 'no-global-site-packages.txt',
|
||||
)
|
||||
return os.path.exists(no_global_site_packages_file)
|
||||
|
||||
|
||||
def virtualenv_no_global():
|
||||
# type: () -> bool
|
||||
"""Returns a boolean, whether running in venv with no system site-packages.
|
||||
"""
|
||||
Return True if in a venv and no system site packages.
|
||||
"""
|
||||
# this mirrors the logic in virtualenv.py for locating the
|
||||
# no-global-site-packages.txt file
|
||||
site_mod_dir = os.path.dirname(os.path.abspath(site.__file__))
|
||||
no_global_file = os.path.join(site_mod_dir, 'no-global-site-packages.txt')
|
||||
if running_under_virtualenv() and os.path.isfile(no_global_file):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
if _running_under_regular_virtualenv():
|
||||
return _no_global_under_regular_virtualenv()
|
||||
|
||||
if _running_under_venv():
|
||||
return _no_global_under_venv()
|
||||
|
||||
return False
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
"""Support functions for working with wheel files.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import logging
|
||||
from email.parser import Parser
|
||||
from zipfile import ZipFile
|
||||
|
||||
from pipenv.patched.notpip._vendor.packaging.utils import canonicalize_name
|
||||
from pipenv.patched.notpip._vendor.pkg_resources import DistInfoDistribution
|
||||
from pipenv.patched.notpip._vendor.six import PY2, ensure_str
|
||||
|
||||
from pipenv.patched.notpip._internal.exceptions import UnsupportedWheel
|
||||
from pipenv.patched.notpip._internal.utils.pkg_resources import DictMetadata
|
||||
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
|
||||
|
||||
if MYPY_CHECK_RUNNING:
|
||||
from email.message import Message
|
||||
from typing import Dict, Tuple
|
||||
|
||||
from pipenv.patched.notpip._vendor.pkg_resources import Distribution
|
||||
|
||||
if PY2:
|
||||
from zipfile import BadZipfile as BadZipFile
|
||||
else:
|
||||
from zipfile import BadZipFile
|
||||
|
||||
|
||||
VERSION_COMPATIBLE = (1, 0)
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WheelMetadata(DictMetadata):
|
||||
"""Metadata provider that maps metadata decoding exceptions to our
|
||||
internal exception type.
|
||||
"""
|
||||
def __init__(self, metadata, wheel_name):
|
||||
# type: (Dict[str, bytes], str) -> None
|
||||
super(WheelMetadata, self).__init__(metadata)
|
||||
self._wheel_name = wheel_name
|
||||
|
||||
def get_metadata(self, name):
|
||||
# type: (str) -> str
|
||||
try:
|
||||
return super(WheelMetadata, self).get_metadata(name)
|
||||
except UnicodeDecodeError as e:
|
||||
# Augment the default error with the origin of the file.
|
||||
raise UnsupportedWheel(
|
||||
"Error decoding metadata for {}: {}".format(
|
||||
self._wheel_name, e
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def pkg_resources_distribution_for_wheel(wheel_zip, name, location):
|
||||
# type: (ZipFile, str, str) -> Distribution
|
||||
"""Get a pkg_resources distribution given a wheel.
|
||||
|
||||
:raises UnsupportedWheel: on any errors
|
||||
"""
|
||||
info_dir, _ = parse_wheel(wheel_zip, name)
|
||||
|
||||
metadata_files = [
|
||||
p for p in wheel_zip.namelist() if p.startswith("{}/".format(info_dir))
|
||||
]
|
||||
|
||||
metadata_text = {} # type: Dict[str, bytes]
|
||||
for path in metadata_files:
|
||||
# If a flag is set, namelist entries may be unicode in Python 2.
|
||||
# We coerce them to native str type to match the types used in the rest
|
||||
# of the code. This cannot fail because unicode can always be encoded
|
||||
# with UTF-8.
|
||||
full_path = ensure_str(path)
|
||||
_, metadata_name = full_path.split("/", 1)
|
||||
|
||||
try:
|
||||
metadata_text[metadata_name] = read_wheel_metadata_file(
|
||||
wheel_zip, full_path
|
||||
)
|
||||
except UnsupportedWheel as e:
|
||||
raise UnsupportedWheel(
|
||||
"{} has an invalid wheel, {}".format(name, str(e))
|
||||
)
|
||||
|
||||
metadata = WheelMetadata(metadata_text, location)
|
||||
|
||||
return DistInfoDistribution(
|
||||
location=location, metadata=metadata, project_name=name
|
||||
)
|
||||
|
||||
|
||||
def parse_wheel(wheel_zip, name):
|
||||
# type: (ZipFile, str) -> Tuple[str, Message]
|
||||
"""Extract information from the provided wheel, ensuring it meets basic
|
||||
standards.
|
||||
|
||||
Returns the name of the .dist-info directory and the parsed WHEEL metadata.
|
||||
"""
|
||||
try:
|
||||
info_dir = wheel_dist_info_dir(wheel_zip, name)
|
||||
metadata = wheel_metadata(wheel_zip, info_dir)
|
||||
version = wheel_version(metadata)
|
||||
except UnsupportedWheel as e:
|
||||
raise UnsupportedWheel(
|
||||
"{} has an invalid wheel, {}".format(name, str(e))
|
||||
)
|
||||
|
||||
check_compatibility(version, name)
|
||||
|
||||
return info_dir, metadata
|
||||
|
||||
|
||||
def wheel_dist_info_dir(source, name):
|
||||
# type: (ZipFile, str) -> str
|
||||
"""Returns the name of the contained .dist-info directory.
|
||||
|
||||
Raises AssertionError or UnsupportedWheel if not found, >1 found, or
|
||||
it doesn't match the provided name.
|
||||
"""
|
||||
# Zip file path separators must be /
|
||||
subdirs = list(set(p.split("/")[0] for p in source.namelist()))
|
||||
|
||||
info_dirs = [s for s in subdirs if s.endswith('.dist-info')]
|
||||
|
||||
if not info_dirs:
|
||||
raise UnsupportedWheel(".dist-info directory not found")
|
||||
|
||||
if len(info_dirs) > 1:
|
||||
raise UnsupportedWheel(
|
||||
"multiple .dist-info directories found: {}".format(
|
||||
", ".join(info_dirs)
|
||||
)
|
||||
)
|
||||
|
||||
info_dir = info_dirs[0]
|
||||
|
||||
info_dir_name = canonicalize_name(info_dir)
|
||||
canonical_name = canonicalize_name(name)
|
||||
if not info_dir_name.startswith(canonical_name):
|
||||
raise UnsupportedWheel(
|
||||
".dist-info directory {!r} does not start with {!r}".format(
|
||||
info_dir, canonical_name
|
||||
)
|
||||
)
|
||||
|
||||
# Zip file paths can be unicode or str depending on the zip entry flags,
|
||||
# so normalize it.
|
||||
return ensure_str(info_dir)
|
||||
|
||||
|
||||
def read_wheel_metadata_file(source, path):
|
||||
# type: (ZipFile, str) -> bytes
|
||||
try:
|
||||
return source.read(path)
|
||||
# BadZipFile for general corruption, KeyError for missing entry,
|
||||
# and RuntimeError for password-protected files
|
||||
except (BadZipFile, KeyError, RuntimeError) as e:
|
||||
raise UnsupportedWheel(
|
||||
"could not read {!r} file: {!r}".format(path, e)
|
||||
)
|
||||
|
||||
|
||||
def wheel_metadata(source, dist_info_dir):
|
||||
# type: (ZipFile, str) -> Message
|
||||
"""Return the WHEEL metadata of an extracted wheel, if possible.
|
||||
Otherwise, raise UnsupportedWheel.
|
||||
"""
|
||||
path = "{}/WHEEL".format(dist_info_dir)
|
||||
# Zip file path separators must be /
|
||||
wheel_contents = read_wheel_metadata_file(source, path)
|
||||
|
||||
try:
|
||||
wheel_text = ensure_str(wheel_contents)
|
||||
except UnicodeDecodeError as e:
|
||||
raise UnsupportedWheel("error decoding {!r}: {!r}".format(path, e))
|
||||
|
||||
# FeedParser (used by Parser) does not raise any exceptions. The returned
|
||||
# message may have .defects populated, but for backwards-compatibility we
|
||||
# currently ignore them.
|
||||
return Parser().parsestr(wheel_text)
|
||||
|
||||
|
||||
def wheel_version(wheel_data):
|
||||
# type: (Message) -> Tuple[int, ...]
|
||||
"""Given WHEEL metadata, return the parsed Wheel-Version.
|
||||
Otherwise, raise UnsupportedWheel.
|
||||
"""
|
||||
version_text = wheel_data["Wheel-Version"]
|
||||
if version_text is None:
|
||||
raise UnsupportedWheel("WHEEL is missing Wheel-Version")
|
||||
|
||||
version = version_text.strip()
|
||||
|
||||
try:
|
||||
return tuple(map(int, version.split('.')))
|
||||
except ValueError:
|
||||
raise UnsupportedWheel("invalid Wheel-Version: {!r}".format(version))
|
||||
|
||||
|
||||
def check_compatibility(version, name):
|
||||
# type: (Tuple[int, ...], str) -> None
|
||||
"""Raises errors or warns if called with an incompatible Wheel-Version.
|
||||
|
||||
Pip should refuse to install a Wheel-Version that's a major series
|
||||
ahead of what it's compatible with (e.g 2.0 > 1.1); and warn when
|
||||
installing a version only minor version ahead (e.g 1.2 > 1.1).
|
||||
|
||||
version: a 2-tuple representing a Wheel-Version (Major, Minor)
|
||||
name: name of wheel or package to raise exception about
|
||||
|
||||
:raises UnsupportedWheel: when an incompatible Wheel-Version is given
|
||||
"""
|
||||
if version[0] > VERSION_COMPATIBLE[0]:
|
||||
raise UnsupportedWheel(
|
||||
"%s's Wheel-Version (%s) is not compatible with this version "
|
||||
"of pip" % (name, '.'.join(map(str, version)))
|
||||
)
|
||||
elif version > VERSION_COMPATIBLE:
|
||||
logger.warning(
|
||||
'Installing from a newer Wheel-Version (%s)',
|
||||
'.'.join(map(str, version)),
|
||||
)
|
||||
@@ -12,7 +12,7 @@ from pipenv.patched.notpip._vendor.six.moves.urllib import parse as urllib_parse
|
||||
from pipenv.patched.notpip._vendor.six.moves.urllib import request as urllib_request
|
||||
|
||||
from pipenv.patched.notpip._internal.exceptions import BadCommand
|
||||
from pipenv.patched.notpip._internal.utils.misc import display_path
|
||||
from pipenv.patched.notpip._internal.utils.misc import display_path, hide_url
|
||||
from pipenv.patched.notpip._internal.utils.subprocess import make_command
|
||||
from pipenv.patched.notpip._internal.utils.temp_dir import TempDirectory
|
||||
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
|
||||
@@ -59,6 +59,23 @@ class Git(VersionControl):
|
||||
def get_base_rev_args(rev):
|
||||
return [rev]
|
||||
|
||||
def is_immutable_rev_checkout(self, url, dest):
|
||||
# type: (str, str) -> bool
|
||||
_, rev_options = self.get_url_rev_options(hide_url(url))
|
||||
if not rev_options.rev:
|
||||
return False
|
||||
if not self.is_commit_id_equal(dest, rev_options.rev):
|
||||
# the current commit is different from rev,
|
||||
# which means rev was something else than a commit hash
|
||||
return False
|
||||
# return False in the rare case rev is both a commit hash
|
||||
# and a tag or a branch; we don't want to cache in that case
|
||||
# because that branch/tag could point to something else in the future
|
||||
is_tag_or_branch = bool(
|
||||
self.get_revision_sha(dest, rev_options.rev)[0]
|
||||
)
|
||||
return not is_tag_or_branch
|
||||
|
||||
def get_git_version(self):
|
||||
VERSION_PFX = 'git version '
|
||||
version = self.run_command(['version'], show_stdout=False)
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
"""Handles all VCS (version control) support"""
|
||||
|
||||
# The following comment should be removed at some point in the future.
|
||||
# mypy: disallow-untyped-defs=False
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import errno
|
||||
@@ -30,7 +27,8 @@ from pipenv.patched.notpip._internal.utils.urls import get_url_scheme
|
||||
|
||||
if MYPY_CHECK_RUNNING:
|
||||
from typing import (
|
||||
Any, Dict, Iterable, List, Mapping, Optional, Text, Tuple, Type, Union
|
||||
Any, Dict, Iterable, Iterator, List, Mapping, Optional, Text, Tuple,
|
||||
Type, Union
|
||||
)
|
||||
from pipenv.patched.notpip._internal.utils.ui import SpinnerInterface
|
||||
from pipenv.patched.notpip._internal.utils.misc import HiddenText
|
||||
@@ -57,6 +55,7 @@ def is_url(name):
|
||||
|
||||
|
||||
def make_vcs_requirement_url(repo_url, rev, project_name, subdir=None):
|
||||
# type: (str, str, str, Optional[str]) -> str
|
||||
"""
|
||||
Return the URL for a VCS requirement.
|
||||
|
||||
@@ -73,6 +72,7 @@ def make_vcs_requirement_url(repo_url, rev, project_name, subdir=None):
|
||||
|
||||
|
||||
def find_path_to_setup_from_repo_root(location, repo_root):
|
||||
# type: (str, str) -> Optional[str]
|
||||
"""
|
||||
Find the path to `setup.py` by searching up the filesystem from `location`.
|
||||
Return the path to `setup.py` relative to `repo_root`.
|
||||
@@ -134,6 +134,7 @@ class RevOptions(object):
|
||||
self.branch_name = None # type: Optional[str]
|
||||
|
||||
def __repr__(self):
|
||||
# type: () -> str
|
||||
return '<RevOptions {}: rev={!r}>'.format(self.vc_class.name, self.rev)
|
||||
|
||||
@property
|
||||
@@ -190,6 +191,7 @@ class VcsSupport(object):
|
||||
super(VcsSupport, self).__init__()
|
||||
|
||||
def __iter__(self):
|
||||
# type: () -> Iterator[str]
|
||||
return self._registry.__iter__()
|
||||
|
||||
@property
|
||||
@@ -237,6 +239,16 @@ class VcsSupport(object):
|
||||
return vcs_backend
|
||||
return None
|
||||
|
||||
def get_backend_for_scheme(self, scheme):
|
||||
# type: (str) -> Optional[VersionControl]
|
||||
"""
|
||||
Return a VersionControl object or None.
|
||||
"""
|
||||
for vcs_backend in self._registry.values():
|
||||
if scheme in vcs_backend.schemes:
|
||||
return vcs_backend
|
||||
return None
|
||||
|
||||
def get_backend(self, name):
|
||||
# type: (str) -> Optional[VersionControl]
|
||||
"""
|
||||
@@ -261,6 +273,7 @@ class VersionControl(object):
|
||||
|
||||
@classmethod
|
||||
def should_add_vcs_url_prefix(cls, remote_url):
|
||||
# type: (str) -> bool
|
||||
"""
|
||||
Return whether the vcs prefix (e.g. "git+") should be added to a
|
||||
repository's remote url when used in a requirement.
|
||||
@@ -269,6 +282,7 @@ class VersionControl(object):
|
||||
|
||||
@classmethod
|
||||
def get_subdirectory(cls, location):
|
||||
# type: (str) -> Optional[str]
|
||||
"""
|
||||
Return the path to setup.py, relative to the repo root.
|
||||
Return None if setup.py is in the repo root.
|
||||
@@ -277,6 +291,7 @@ class VersionControl(object):
|
||||
|
||||
@classmethod
|
||||
def get_requirement_revision(cls, repo_dir):
|
||||
# type: (str) -> str
|
||||
"""
|
||||
Return the revision string that should be used in a requirement.
|
||||
"""
|
||||
@@ -284,6 +299,7 @@ class VersionControl(object):
|
||||
|
||||
@classmethod
|
||||
def get_src_requirement(cls, repo_dir, project_name):
|
||||
# type: (str, str) -> Optional[str]
|
||||
"""
|
||||
Return the requirement string to use to redownload the files
|
||||
currently at the given repository directory.
|
||||
@@ -311,6 +327,7 @@ class VersionControl(object):
|
||||
|
||||
@staticmethod
|
||||
def get_base_rev_args(rev):
|
||||
# type: (str) -> List[str]
|
||||
"""
|
||||
Return the base revision arguments for a vcs command.
|
||||
|
||||
@@ -319,6 +336,20 @@ class VersionControl(object):
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def is_immutable_rev_checkout(self, url, dest):
|
||||
# type: (str, str) -> bool
|
||||
"""
|
||||
Return true if the commit hash checked out at dest matches
|
||||
the revision in url.
|
||||
|
||||
Always return False, if the VCS does not support immutable commit
|
||||
hashes.
|
||||
|
||||
This method does not check if there are local uncommitted changes
|
||||
in dest after checkout, as pip currently has no use case for that.
|
||||
"""
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def make_rev_options(cls, rev=None, extra_args=None):
|
||||
# type: (Optional[str], Optional[CommandArgs]) -> RevOptions
|
||||
@@ -353,6 +384,7 @@ class VersionControl(object):
|
||||
|
||||
@classmethod
|
||||
def get_netloc_and_auth(cls, netloc, scheme):
|
||||
# type: (str, str) -> Tuple[str, Tuple[Optional[str], Optional[str]]]
|
||||
"""
|
||||
Parse the repository URL's netloc, and return the new netloc to use
|
||||
along with auth information.
|
||||
@@ -470,6 +502,7 @@ class VersionControl(object):
|
||||
|
||||
@classmethod
|
||||
def is_commit_id_equal(cls, dest, name):
|
||||
# type: (str, Optional[str]) -> bool
|
||||
"""
|
||||
Return whether the id of the current commit equals the given name.
|
||||
|
||||
@@ -586,6 +619,7 @@ class VersionControl(object):
|
||||
|
||||
@classmethod
|
||||
def get_remote_url(cls, location):
|
||||
# type: (str) -> str
|
||||
"""
|
||||
Return the url used at location
|
||||
|
||||
@@ -596,6 +630,7 @@ class VersionControl(object):
|
||||
|
||||
@classmethod
|
||||
def get_revision(cls, location):
|
||||
# type: (str) -> str
|
||||
"""
|
||||
Return the current commit id of the files at the given location.
|
||||
"""
|
||||
@@ -612,7 +647,7 @@ class VersionControl(object):
|
||||
command_desc=None, # type: Optional[str]
|
||||
extra_environ=None, # type: Optional[Mapping[str, Any]]
|
||||
spinner=None, # type: Optional[SpinnerInterface]
|
||||
log_failed_cmd=True
|
||||
log_failed_cmd=True # type: bool
|
||||
):
|
||||
# type: (...) -> Text
|
||||
"""
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,305 @@
|
||||
"""Orchestrator for building wheels from InstallRequirements.
|
||||
"""
|
||||
|
||||
# The following comment should be removed at some point in the future.
|
||||
# mypy: strict-optional=False
|
||||
|
||||
import logging
|
||||
import os.path
|
||||
import re
|
||||
import shutil
|
||||
|
||||
from pipenv.patched.notpip._internal.models.link import Link
|
||||
from pipenv.patched.notpip._internal.operations.build.wheel import build_wheel_pep517
|
||||
from pipenv.patched.notpip._internal.operations.build.wheel_legacy import build_wheel_legacy
|
||||
from pipenv.patched.notpip._internal.utils.logging import indent_log
|
||||
from pipenv.patched.notpip._internal.utils.misc import ensure_dir, hash_file, is_wheel_installed
|
||||
from pipenv.patched.notpip._internal.utils.setuptools_build import make_setuptools_clean_args
|
||||
from pipenv.patched.notpip._internal.utils.subprocess import call_subprocess
|
||||
from pipenv.patched.notpip._internal.utils.temp_dir import TempDirectory
|
||||
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
|
||||
from pipenv.patched.notpip._internal.utils.urls import path_to_url
|
||||
from pipenv.patched.notpip._internal.vcs import vcs
|
||||
|
||||
if MYPY_CHECK_RUNNING:
|
||||
from typing import (
|
||||
Any, Callable, Iterable, List, Optional, Pattern, Tuple,
|
||||
)
|
||||
|
||||
from pipenv.patched.notpip._internal.cache import WheelCache
|
||||
from pipenv.patched.notpip._internal.req.req_install import InstallRequirement
|
||||
|
||||
BinaryAllowedPredicate = Callable[[InstallRequirement], bool]
|
||||
BuildResult = Tuple[List[InstallRequirement], List[InstallRequirement]]
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _contains_egg_info(
|
||||
s, _egg_info_re=re.compile(r'([a-z0-9_.]+)-([a-z0-9_.!+-]+)', re.I)):
|
||||
# type: (str, Pattern[str]) -> bool
|
||||
"""Determine whether the string looks like an egg_info.
|
||||
|
||||
:param s: The string to parse. E.g. foo-2.1
|
||||
"""
|
||||
return bool(_egg_info_re.search(s))
|
||||
|
||||
|
||||
def _should_build(
|
||||
req, # type: InstallRequirement
|
||||
need_wheel, # type: bool
|
||||
check_binary_allowed, # type: BinaryAllowedPredicate
|
||||
):
|
||||
# type: (...) -> bool
|
||||
"""Return whether an InstallRequirement should be built into a wheel."""
|
||||
if req.constraint:
|
||||
# never build requirements that are merely constraints
|
||||
return False
|
||||
if req.is_wheel:
|
||||
if need_wheel:
|
||||
logger.info(
|
||||
'Skipping %s, due to already being wheel.', req.name,
|
||||
)
|
||||
return False
|
||||
|
||||
if need_wheel:
|
||||
# i.e. pip wheel, not pip install
|
||||
return True
|
||||
|
||||
# From this point, this concerns the pip install command only
|
||||
# (need_wheel=False).
|
||||
|
||||
if not req.use_pep517 and not is_wheel_installed():
|
||||
# we don't build legacy requirements if wheel is not installed
|
||||
return False
|
||||
|
||||
if req.editable or not req.source_dir:
|
||||
return False
|
||||
|
||||
if not check_binary_allowed(req):
|
||||
logger.info(
|
||||
"Skipping wheel build for %s, due to binaries "
|
||||
"being disabled for it.", req.name,
|
||||
)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def should_build_for_wheel_command(
|
||||
req, # type: InstallRequirement
|
||||
):
|
||||
# type: (...) -> bool
|
||||
return _should_build(
|
||||
req, need_wheel=True, check_binary_allowed=_always_true
|
||||
)
|
||||
|
||||
|
||||
def should_build_for_install_command(
|
||||
req, # type: InstallRequirement
|
||||
check_binary_allowed, # type: BinaryAllowedPredicate
|
||||
):
|
||||
# type: (...) -> bool
|
||||
return _should_build(
|
||||
req, need_wheel=False, check_binary_allowed=check_binary_allowed
|
||||
)
|
||||
|
||||
|
||||
def _should_cache(
|
||||
req, # type: InstallRequirement
|
||||
):
|
||||
# type: (...) -> Optional[bool]
|
||||
"""
|
||||
Return whether a built InstallRequirement can be stored in the persistent
|
||||
wheel cache, assuming the wheel cache is available, and _should_build()
|
||||
has determined a wheel needs to be built.
|
||||
"""
|
||||
if not should_build_for_install_command(
|
||||
req, check_binary_allowed=_always_true
|
||||
):
|
||||
# never cache if pip install would not have built
|
||||
# (editable mode, etc)
|
||||
return False
|
||||
|
||||
if req.link and req.link.is_vcs:
|
||||
# VCS checkout. Do not cache
|
||||
# unless it points to an immutable commit hash.
|
||||
assert not req.editable
|
||||
assert req.source_dir
|
||||
vcs_backend = vcs.get_backend_for_scheme(req.link.scheme)
|
||||
assert vcs_backend
|
||||
if vcs_backend.is_immutable_rev_checkout(req.link.url, req.source_dir):
|
||||
return True
|
||||
return False
|
||||
|
||||
base, ext = req.link.splitext()
|
||||
if _contains_egg_info(base):
|
||||
return True
|
||||
|
||||
# Otherwise, do not cache.
|
||||
return False
|
||||
|
||||
|
||||
def _get_cache_dir(
|
||||
req, # type: InstallRequirement
|
||||
wheel_cache, # type: WheelCache
|
||||
):
|
||||
# type: (...) -> str
|
||||
"""Return the persistent or temporary cache directory where the built
|
||||
wheel need to be stored.
|
||||
"""
|
||||
cache_available = bool(wheel_cache.cache_dir)
|
||||
if cache_available and _should_cache(req):
|
||||
cache_dir = wheel_cache.get_path_for_link(req.link)
|
||||
else:
|
||||
cache_dir = wheel_cache.get_ephem_path_for_link(req.link)
|
||||
return cache_dir
|
||||
|
||||
|
||||
def _always_true(_):
|
||||
# type: (Any) -> bool
|
||||
return True
|
||||
|
||||
|
||||
def _build_one(
|
||||
req, # type: InstallRequirement
|
||||
output_dir, # type: str
|
||||
build_options, # type: List[str]
|
||||
global_options, # type: List[str]
|
||||
):
|
||||
# type: (...) -> Optional[str]
|
||||
"""Build one wheel.
|
||||
|
||||
:return: The filename of the built wheel, or None if the build failed.
|
||||
"""
|
||||
try:
|
||||
ensure_dir(output_dir)
|
||||
except OSError as e:
|
||||
logger.warning(
|
||||
"Building wheel for %s failed: %s",
|
||||
req.name, e,
|
||||
)
|
||||
return None
|
||||
|
||||
# Install build deps into temporary directory (PEP 518)
|
||||
with req.build_env:
|
||||
return _build_one_inside_env(
|
||||
req, output_dir, build_options, global_options
|
||||
)
|
||||
|
||||
|
||||
def _build_one_inside_env(
|
||||
req, # type: InstallRequirement
|
||||
output_dir, # type: str
|
||||
build_options, # type: List[str]
|
||||
global_options, # type: List[str]
|
||||
):
|
||||
# type: (...) -> Optional[str]
|
||||
with TempDirectory(kind="wheel") as temp_dir:
|
||||
if req.use_pep517:
|
||||
wheel_path = build_wheel_pep517(
|
||||
name=req.name,
|
||||
backend=req.pep517_backend,
|
||||
metadata_directory=req.metadata_directory,
|
||||
build_options=build_options,
|
||||
tempd=temp_dir.path,
|
||||
)
|
||||
else:
|
||||
wheel_path = build_wheel_legacy(
|
||||
name=req.name,
|
||||
setup_py_path=req.setup_py_path,
|
||||
source_dir=req.unpacked_source_directory,
|
||||
global_options=global_options,
|
||||
build_options=build_options,
|
||||
tempd=temp_dir.path,
|
||||
)
|
||||
|
||||
if wheel_path is not None:
|
||||
wheel_name = os.path.basename(wheel_path)
|
||||
dest_path = os.path.join(output_dir, wheel_name)
|
||||
try:
|
||||
wheel_hash, length = hash_file(wheel_path)
|
||||
shutil.move(wheel_path, dest_path)
|
||||
logger.info('Created wheel for %s: '
|
||||
'filename=%s size=%d sha256=%s',
|
||||
req.name, wheel_name, length,
|
||||
wheel_hash.hexdigest())
|
||||
logger.info('Stored in directory: %s', output_dir)
|
||||
return dest_path
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
"Building wheel for %s failed: %s",
|
||||
req.name, e,
|
||||
)
|
||||
# Ignore return, we can't do anything else useful.
|
||||
if not req.use_pep517:
|
||||
_clean_one_legacy(req, global_options)
|
||||
return None
|
||||
|
||||
|
||||
def _clean_one_legacy(req, global_options):
|
||||
# type: (InstallRequirement, List[str]) -> bool
|
||||
clean_args = make_setuptools_clean_args(
|
||||
req.setup_py_path,
|
||||
global_options=global_options,
|
||||
)
|
||||
|
||||
logger.info('Running setup.py clean for %s', req.name)
|
||||
try:
|
||||
call_subprocess(clean_args, cwd=req.source_dir)
|
||||
return True
|
||||
except Exception:
|
||||
logger.error('Failed cleaning build dir for %s', req.name)
|
||||
return False
|
||||
|
||||
|
||||
def build(
|
||||
requirements, # type: Iterable[InstallRequirement]
|
||||
wheel_cache, # type: WheelCache
|
||||
build_options, # type: List[str]
|
||||
global_options, # type: List[str]
|
||||
):
|
||||
# type: (...) -> BuildResult
|
||||
"""Build wheels.
|
||||
|
||||
:return: The list of InstallRequirement that succeeded to build and
|
||||
the list of InstallRequirement that failed to build.
|
||||
"""
|
||||
if not requirements:
|
||||
return [], []
|
||||
|
||||
# Build the wheels.
|
||||
logger.info(
|
||||
'Building wheels for collected packages: %s',
|
||||
', '.join(req.name for req in requirements),
|
||||
)
|
||||
|
||||
with indent_log():
|
||||
build_successes, build_failures = [], []
|
||||
for req in requirements:
|
||||
cache_dir = _get_cache_dir(req, wheel_cache)
|
||||
wheel_file = _build_one(
|
||||
req, cache_dir, build_options, global_options
|
||||
)
|
||||
if wheel_file:
|
||||
# Update the link for this.
|
||||
req.link = Link(path_to_url(wheel_file))
|
||||
req.local_file_path = req.link.file_path
|
||||
assert req.link.is_wheel
|
||||
build_successes.append(req)
|
||||
else:
|
||||
build_failures.append(req)
|
||||
|
||||
# notify success/failure
|
||||
if build_successes:
|
||||
logger.info(
|
||||
'Successfully built %s',
|
||||
' '.join([req.name for req in build_successes]),
|
||||
)
|
||||
if build_failures:
|
||||
logger.info(
|
||||
'Failed to build %s',
|
||||
' '.join([req.name for req in build_failures]),
|
||||
)
|
||||
# Return a list of requirements that failed to build
|
||||
return build_successes, build_failures
|
||||
@@ -37,6 +37,10 @@ if sys.platform.startswith('java'):
|
||||
# are actually checked for and the rest of the module expects
|
||||
# *sys.platform* style strings.
|
||||
system = 'linux2'
|
||||
elif sys.platform == 'cli' and os.name == 'nt':
|
||||
# Detect Windows in IronPython to match pip._internal.utils.compat.WINDOWS
|
||||
# Discussion: <https://github.com/pypa/pip/pull/7501>
|
||||
system = 'win32'
|
||||
else:
|
||||
system = sys.platform
|
||||
|
||||
@@ -64,7 +68,7 @@ def user_data_dir(appname=None, appauthor=None, version=None, roaming=False):
|
||||
for a discussion of issues.
|
||||
|
||||
Typical user data directories are:
|
||||
Mac OS X: ~/Library/Application Support/<AppName>
|
||||
Mac OS X: ~/Library/Application Support/<AppName> # or ~/.config/<AppName>, if the other does not exist
|
||||
Unix: ~/.local/share/<AppName> # or in $XDG_DATA_HOME, if defined
|
||||
Win XP (not roaming): C:\Documents and Settings\<username>\Application Data\<AppAuthor>\<AppName>
|
||||
Win XP (roaming): C:\Documents and Settings\<username>\Local Settings\Application Data\<AppAuthor>\<AppName>
|
||||
@@ -88,6 +92,10 @@ def user_data_dir(appname=None, appauthor=None, version=None, roaming=False):
|
||||
path = os.path.expanduser('~/Library/Application Support/')
|
||||
if appname:
|
||||
path = os.path.join(path, appname)
|
||||
if not os.path.isdir(path):
|
||||
path = os.path.expanduser('~/.config/')
|
||||
if appname:
|
||||
path = os.path.join(path, appname)
|
||||
else:
|
||||
path = os.getenv('XDG_DATA_HOME', os.path.expanduser("~/.local/share"))
|
||||
if appname:
|
||||
@@ -150,7 +158,7 @@ def site_data_dir(appname=None, appauthor=None, version=None, multipath=False):
|
||||
if appname:
|
||||
if version:
|
||||
appname = os.path.join(appname, version)
|
||||
pathlist = [os.sep.join([x, appname]) for x in pathlist]
|
||||
pathlist = [os.path.join(x, appname) for x in pathlist]
|
||||
|
||||
if multipath:
|
||||
path = os.pathsep.join(pathlist)
|
||||
@@ -203,6 +211,8 @@ def user_config_dir(appname=None, appauthor=None, version=None, roaming=False):
|
||||
return path
|
||||
|
||||
|
||||
# for the discussion regarding site_config_dir locations
|
||||
# see <https://github.com/pypa/pip/issues/1733>
|
||||
def site_config_dir(appname=None, appauthor=None, version=None, multipath=False):
|
||||
r"""Return full path to the user-shared data dir for this application.
|
||||
|
||||
@@ -238,14 +248,17 @@ def site_config_dir(appname=None, appauthor=None, version=None, multipath=False)
|
||||
if appname and version:
|
||||
path = os.path.join(path, version)
|
||||
else:
|
||||
# XDG default for $XDG_CONFIG_DIRS
|
||||
# XDG default for $XDG_CONFIG_DIRS (missing or empty)
|
||||
# see <https://github.com/pypa/pip/pull/7501#discussion_r360624829>
|
||||
# only first, if multipath is False
|
||||
path = os.getenv('XDG_CONFIG_DIRS', '/etc/xdg')
|
||||
pathlist = [os.path.expanduser(x.rstrip(os.sep)) for x in path.split(os.pathsep)]
|
||||
path = os.getenv('XDG_CONFIG_DIRS') or '/etc/xdg'
|
||||
pathlist = [os.path.expanduser(x.rstrip(os.sep)) for x in path.split(os.pathsep) if x]
|
||||
if appname:
|
||||
if version:
|
||||
appname = os.path.join(appname, version)
|
||||
pathlist = [os.sep.join([x, appname]) for x in pathlist]
|
||||
pathlist = [os.path.join(x, appname) for x in pathlist]
|
||||
# always look in /etc directly as well
|
||||
pathlist.append('/etc')
|
||||
|
||||
if multipath:
|
||||
path = os.pathsep.join(pathlist)
|
||||
@@ -291,6 +304,10 @@ def user_cache_dir(appname=None, appauthor=None, version=None, opinion=True):
|
||||
if appauthor is None:
|
||||
appauthor = appname
|
||||
path = os.path.normpath(_get_win_folder("CSIDL_LOCAL_APPDATA"))
|
||||
# When using Python 2, return paths as bytes on Windows like we do on
|
||||
# other operating systems. See helper function docs for more details.
|
||||
if not PY3 and isinstance(path, unicode):
|
||||
path = _win_path_to_bytes(path)
|
||||
if appname:
|
||||
if appauthor is not False:
|
||||
path = os.path.join(path, appauthor, appname)
|
||||
@@ -567,6 +584,24 @@ if system == "win32":
|
||||
_get_win_folder = _get_win_folder_from_registry
|
||||
|
||||
|
||||
def _win_path_to_bytes(path):
|
||||
"""Encode Windows paths to bytes. Only used on Python 2.
|
||||
|
||||
Motivation is to be consistent with other operating systems where paths
|
||||
are also returned as bytes. This avoids problems mixing bytes and Unicode
|
||||
elsewhere in the codebase. For more details and discussion see
|
||||
<https://github.com/pypa/pip/issues/3463>.
|
||||
|
||||
If encoding using ASCII and MBCS fails, return the original Unicode path.
|
||||
"""
|
||||
for encoding in ('ASCII', 'MBCS'):
|
||||
try:
|
||||
return path.encode(encoding)
|
||||
except (UnicodeEncodeError, LookupError):
|
||||
pass
|
||||
return path
|
||||
|
||||
|
||||
#---- self test code
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -4,7 +4,7 @@ Make it easy to import from cachecontrol without long namespaces.
|
||||
"""
|
||||
__author__ = "Eric Larson"
|
||||
__email__ = "eric@ionrock.org"
|
||||
__version__ = "0.12.5"
|
||||
__version__ = "0.12.6"
|
||||
|
||||
from .wrapper import CacheControl
|
||||
from .adapter import CacheControlAdapter
|
||||
|
||||
@@ -24,7 +24,7 @@ class CacheControlAdapter(HTTPAdapter):
|
||||
**kw
|
||||
):
|
||||
super(CacheControlAdapter, self).__init__(*args, **kw)
|
||||
self.cache = cache or DictCache()
|
||||
self.cache = DictCache() if cache is None else cache
|
||||
self.heuristic = heuristic
|
||||
self.cacheable_methods = cacheable_methods or ("GET",)
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ class CacheController(object):
|
||||
def __init__(
|
||||
self, cache=None, cache_etags=True, serializer=None, status_codes=None
|
||||
):
|
||||
self.cache = cache or DictCache()
|
||||
self.cache = DictCache() if cache is None else cache
|
||||
self.cache_etags = cache_etags
|
||||
self.serializer = serializer or Serializer()
|
||||
self.cacheable_status_codes = status_codes or (200, 203, 300, 301)
|
||||
@@ -293,6 +293,15 @@ class CacheController(object):
|
||||
if no_store:
|
||||
return
|
||||
|
||||
# https://tools.ietf.org/html/rfc7234#section-4.1:
|
||||
# A Vary header field-value of "*" always fails to match.
|
||||
# Storing such a response leads to a deserialization warning
|
||||
# during cache lookup and is not allowed to ever be served,
|
||||
# so storing it can be avoided.
|
||||
if "*" in response_headers.get("vary", ""):
|
||||
logger.debug('Response header has "Vary: *"')
|
||||
return
|
||||
|
||||
# If we've been given an etag, then keep the response
|
||||
if self.cache_etags and "etag" in response_headers:
|
||||
logger.debug("Caching due to etag")
|
||||
|
||||
@@ -107,6 +107,8 @@ class Serializer(object):
|
||||
"""
|
||||
# Special case the '*' Vary value as it means we cannot actually
|
||||
# determine if the cached response is suitable for this request.
|
||||
# This case is also handled in the controller code when creating
|
||||
# a cache entry, but is left here for backwards compatibility.
|
||||
if "*" in cached.get("vary", {}):
|
||||
return
|
||||
|
||||
@@ -179,7 +181,7 @@ class Serializer(object):
|
||||
|
||||
def _loads_v4(self, request, data):
|
||||
try:
|
||||
cached = msgpack.loads(data, encoding="utf-8")
|
||||
cached = msgpack.loads(data, raw=False)
|
||||
except ValueError:
|
||||
return
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ def CacheControl(
|
||||
cacheable_methods=None,
|
||||
):
|
||||
|
||||
cache = cache or DictCache()
|
||||
cache = DictCache() if cache is None else cache
|
||||
adapter_class = adapter_class or CacheControlAdapter
|
||||
adapter = adapter_class(
|
||||
cache,
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
from .core import where
|
||||
|
||||
__version__ = "2019.09.11"
|
||||
__version__ = "2019.11.28"
|
||||
|
||||
@@ -4556,3 +4556,47 @@ L5/ndtFhKvshuzHQqp9HpLIiyhY6UFfEW0NnxWViA0kB60PZ2Pierc+xYw5F9KBa
|
||||
LJstxabArahH9CdMOA0uG0k7UvToiIMrVCjU8jVStDKDYmlkDJGcn5fqdBb9HxEG
|
||||
mpv0
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
# Issuer: CN=Entrust Root Certification Authority - G4 O=Entrust, Inc. OU=See www.entrust.net/legal-terms/(c) 2015 Entrust, Inc. - for authorized use only
|
||||
# Subject: CN=Entrust Root Certification Authority - G4 O=Entrust, Inc. OU=See www.entrust.net/legal-terms/(c) 2015 Entrust, Inc. - for authorized use only
|
||||
# Label: "Entrust Root Certification Authority - G4"
|
||||
# Serial: 289383649854506086828220374796556676440
|
||||
# MD5 Fingerprint: 89:53:f1:83:23:b7:7c:8e:05:f1:8c:71:38:4e:1f:88
|
||||
# SHA1 Fingerprint: 14:88:4e:86:26:37:b0:26:af:59:62:5c:40:77:ec:35:29:ba:96:01
|
||||
# SHA256 Fingerprint: db:35:17:d1:f6:73:2a:2d:5a:b9:7c:53:3e:c7:07:79:ee:32:70:a6:2f:b4:ac:42:38:37:24:60:e6:f0:1e:88
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIGSzCCBDOgAwIBAgIRANm1Q3+vqTkPAAAAAFVlrVgwDQYJKoZIhvcNAQELBQAw
|
||||
gb4xCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQL
|
||||
Ex9TZWUgd3d3LmVudHJ1c3QubmV0L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykg
|
||||
MjAxNSBFbnRydXN0LCBJbmMuIC0gZm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxMjAw
|
||||
BgNVBAMTKUVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEc0
|
||||
MB4XDTE1MDUyNzExMTExNloXDTM3MTIyNzExNDExNlowgb4xCzAJBgNVBAYTAlVT
|
||||
MRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1
|
||||
c3QubmV0L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxNSBFbnRydXN0LCBJ
|
||||
bmMuIC0gZm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxMjAwBgNVBAMTKUVudHJ1c3Qg
|
||||
Um9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEc0MIICIjANBgkqhkiG9w0B
|
||||
AQEFAAOCAg8AMIICCgKCAgEAsewsQu7i0TD/pZJH4i3DumSXbcr3DbVZwbPLqGgZ
|
||||
2K+EbTBwXX7zLtJTmeH+H17ZSK9dE43b/2MzTdMAArzE+NEGCJR5WIoV3imz/f3E
|
||||
T+iq4qA7ec2/a0My3dl0ELn39GjUu9CH1apLiipvKgS1sqbHoHrmSKvS0VnM1n4j
|
||||
5pds8ELl3FFLFUHtSUrJ3hCX1nbB76W1NhSXNdh4IjVS70O92yfbYVaCNNzLiGAM
|
||||
C1rlLAHGVK/XqsEQe9IFWrhAnoanw5CGAlZSCXqc0ieCU0plUmr1POeo8pyvi73T
|
||||
DtTUXm6Hnmo9RR3RXRv06QqsYJn7ibT/mCzPfB3pAqoEmh643IhuJbNsZvc8kPNX
|
||||
wbMv9W3y+8qh+CmdRouzavbmZwe+LGcKKh9asj5XxNMhIWNlUpEbsZmOeX7m640A
|
||||
2Vqq6nPopIICR5b+W45UYaPrL0swsIsjdXJ8ITzI9vF01Bx7owVV7rtNOzK+mndm
|
||||
nqxpkCIHH2E6lr7lmk/MBTwoWdPBDFSoWWG9yHJM6Nyfh3+9nEg2XpWjDrk4JFX8
|
||||
dWbrAuMINClKxuMrLzOg2qOGpRKX/YAr2hRC45K9PvJdXmd0LhyIRyk0X+IyqJwl
|
||||
N4y6mACXi0mWHv0liqzc2thddG5msP9E36EYxr5ILzeUePiVSj9/E15dWf10hkNj
|
||||
c0kCAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYD
|
||||
VR0OBBYEFJ84xFYjwznooHFs6FRM5Og6sb9nMA0GCSqGSIb3DQEBCwUAA4ICAQAS
|
||||
5UKme4sPDORGpbZgQIeMJX6tuGguW8ZAdjwD+MlZ9POrYs4QjbRaZIxowLByQzTS
|
||||
Gwv2LFPSypBLhmb8qoMi9IsabyZIrHZ3CL/FmFz0Jomee8O5ZDIBf9PD3Vht7LGr
|
||||
hFV0d4QEJ1JrhkzO3bll/9bGXp+aEJlLdWr+aumXIOTkdnrG0CSqkM0gkLpHZPt/
|
||||
B7NTeLUKYvJzQ85BK4FqLoUWlFPUa19yIqtRLULVAJyZv967lDtX/Zr1hstWO1uI
|
||||
AeV8KEsD+UmDfLJ/fOPtjqF/YFOOVZ1QNBIPt5d7bIdKROf1beyAN/BYGW5KaHbw
|
||||
H5Lk6rWS02FREAutp9lfx1/cH6NcjKF+m7ee01ZvZl4HliDtC3T7Zk6LERXpgUl+
|
||||
b7DUUH8i119lAg2m9IUe2K4GS0qn0jFmwvjO5QimpAKWRGhXxNUzzxkvFMSUHHuk
|
||||
2fCfDrGA4tGeEWSpiBE6doLlYsKA2KSD7ZPvfC+QsDJMlhVoSFLUmQjAJOgc47Ol
|
||||
IQ6SwJAfzyBfyjs4x7dtOvPmRLgOMWuIjnDrnBdSqEGULoe256YSxXXfW8AKbnuk
|
||||
5F6G+TaU33fD6Q3AOfF5u0aOq0NZJ7cguyPpVkAh7DE9ZapD8j3fcEThuk0mEDuY
|
||||
n/PIjhs4ViFqUZPTkcpG2om3PVODLAgfi49T3f+sHw==
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
@@ -3,4 +3,4 @@ from .initialise import init, deinit, reinit, colorama_text
|
||||
from .ansi import Fore, Back, Style, Cursor
|
||||
from .ansitowin32 import AnsiToWin32
|
||||
|
||||
__version__ = '0.4.1'
|
||||
__version__ = '0.4.3'
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#
|
||||
import logging
|
||||
|
||||
__version__ = '0.2.9.post0'
|
||||
__version__ = '0.3.0'
|
||||
|
||||
class DistlibException(Exception):
|
||||
pass
|
||||
|
||||
@@ -119,11 +119,9 @@ def _expand_globals(config):
|
||||
|
||||
#_expand_globals(_SCHEMES)
|
||||
|
||||
# FIXME don't rely on sys.version here, its format is an implementation detail
|
||||
# of CPython, use sys.version_info or sys.hexversion
|
||||
_PY_VERSION = sys.version.split()[0]
|
||||
_PY_VERSION_SHORT = sys.version[:3]
|
||||
_PY_VERSION_SHORT_NO_DOT = _PY_VERSION[0] + _PY_VERSION[2]
|
||||
_PY_VERSION = '%s.%s.%s' % sys.version_info[:3]
|
||||
_PY_VERSION_SHORT = '%s.%s' % sys.version_info[:2]
|
||||
_PY_VERSION_SHORT_NO_DOT = '%s%s' % sys.version_info[:2]
|
||||
_PREFIX = os.path.normpath(sys.prefix)
|
||||
_EXEC_PREFIX = os.path.normpath(sys.exec_prefix)
|
||||
_CONFIG_VARS = None
|
||||
|
||||
@@ -567,7 +567,7 @@ class InstalledDistribution(BaseInstalledDistribution):
|
||||
p = os.path.join(path, 'top_level.txt')
|
||||
if os.path.exists(p):
|
||||
with open(p, 'rb') as f:
|
||||
data = f.read()
|
||||
data = f.read().decode('utf-8')
|
||||
self.modules = data.splitlines()
|
||||
|
||||
def __repr__(self):
|
||||
|
||||
@@ -304,18 +304,25 @@ class Locator(object):
|
||||
|
||||
def _get_digest(self, info):
|
||||
"""
|
||||
Get a digest from a dictionary by looking at keys of the form
|
||||
'algo_digest'.
|
||||
Get a digest from a dictionary by looking at a "digests" dictionary
|
||||
or keys of the form 'algo_digest'.
|
||||
|
||||
Returns a 2-tuple (algo, digest) if found, else None. Currently
|
||||
looks only for SHA256, then MD5.
|
||||
"""
|
||||
result = None
|
||||
for algo in ('sha256', 'md5'):
|
||||
key = '%s_digest' % algo
|
||||
if key in info:
|
||||
result = (algo, info[key])
|
||||
break
|
||||
if 'digests' in info:
|
||||
digests = info['digests']
|
||||
for algo in ('sha256', 'md5'):
|
||||
if algo in digests:
|
||||
result = (algo, digests[algo])
|
||||
break
|
||||
if not result:
|
||||
for algo in ('sha256', 'md5'):
|
||||
key = '%s_digest' % algo
|
||||
if key in info:
|
||||
result = (algo, info[key])
|
||||
break
|
||||
return result
|
||||
|
||||
def _update_version_data(self, result, info):
|
||||
|
||||
@@ -172,8 +172,16 @@ class ScriptMaker(object):
|
||||
|
||||
if sys.platform.startswith('java'): # pragma: no cover
|
||||
executable = self._fix_jython_executable(executable)
|
||||
# Normalise case for Windows
|
||||
executable = os.path.normcase(executable)
|
||||
|
||||
# Normalise case for Windows - COMMENTED OUT
|
||||
# executable = os.path.normcase(executable)
|
||||
# N.B. The normalising operation above has been commented out: See
|
||||
# issue #124. Although paths in Windows are generally case-insensitive,
|
||||
# they aren't always. For example, a path containing a ẞ (which is a
|
||||
# LATIN CAPITAL LETTER SHARP S - U+1E9E) is normcased to ß (which is a
|
||||
# LATIN SMALL LETTER SHARP S' - U+00DF). The two are not considered by
|
||||
# Windows as equivalent in path names.
|
||||
|
||||
# If the user didn't specify an executable, it may be necessary to
|
||||
# cater for executable paths with spaces (not uncommon on Windows)
|
||||
if enquote:
|
||||
@@ -285,9 +293,10 @@ class ScriptMaker(object):
|
||||
if '' in self.variants:
|
||||
scriptnames.add(name)
|
||||
if 'X' in self.variants:
|
||||
scriptnames.add('%s%s' % (name, sys.version[0]))
|
||||
scriptnames.add('%s%s' % (name, sys.version_info[0]))
|
||||
if 'X.Y' in self.variants:
|
||||
scriptnames.add('%s-%s' % (name, sys.version[:3]))
|
||||
scriptnames.add('%s-%s.%s' % (name, sys.version_info[0],
|
||||
sys.version_info[1]))
|
||||
if options and options.get('gui', False):
|
||||
ext = 'pyw'
|
||||
else:
|
||||
@@ -367,8 +376,12 @@ class ScriptMaker(object):
|
||||
# Issue 31: don't hardcode an absolute package name, but
|
||||
# determine it relative to the current package
|
||||
distlib_package = __name__.rsplit('.', 1)[0]
|
||||
result = finder(distlib_package).find(name).bytes
|
||||
return result
|
||||
resource = finder(distlib_package).find(name)
|
||||
if not resource:
|
||||
msg = ('Unable to find resource %s in package %s' % (name,
|
||||
distlib_package))
|
||||
raise ValueError(msg)
|
||||
return resource.bytes
|
||||
|
||||
# Public API follows
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user