Merge pull request #4215 from pypa/feature/update-pip-piptools

This commit is contained in:
Dan Ryan
2020-04-28 16:09:43 -04:00
committed by GitHub
168 changed files with 7119 additions and 6162 deletions
+1
View File
@@ -0,0 +1 @@
Updated vendored ``pip`` => ``20.0.2`` and ``pip-tools`` => ``5.0.0``.
+18 -1
View File
@@ -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)
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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__)
+108 -32
View File
@@ -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
@@ -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
-578
View File
@@ -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
"""
@@ -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,
)
@@ -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,
+58 -20
View File
@@ -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"],
)
+10 -41
View File
@@ -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)
@@ -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 '
+117 -401
View File
@@ -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()
+28 -3
View File
@@ -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)
+20 -10
View File
@@ -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)
+263 -120
View File
@@ -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)
+203 -339
View File
@@ -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 "
+17 -252
View File
@@ -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]
+20 -48
View File
@@ -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'
)
+3 -28
View File
@@ -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))
+41 -25
View File
@@ -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
+3 -3
View File
@@ -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)),
)
+18 -1
View File
@@ -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
+41 -6
View File
@@ -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