Update to pip 10.0.1 and update imports

Signed-off-by: Dan Ryan <dan@danryan.co>
This commit is contained in:
Dan Ryan
2018-05-18 00:46:20 -04:00
parent 01149ce21e
commit 81ae2628e5
190 changed files with 9661 additions and 10594 deletions
+20 -20
View File
@@ -1,20 +1,20 @@
Copyright (c) 2008-2016 The pip developers (see AUTHORS.txt file)
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Copyright (c) 2008-2016 The pip developers (see AUTHORS.txt file)
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+1 -346
View File
@@ -1,346 +1 @@
#!/usr/bin/env python
from __future__ import absolute_import
import locale
import logging
import os
import optparse
import warnings
import sys
import re
# 2016-06-17 barry@debian.org: urllib3 1.14 added optional support for socks,
# but if invoked (i.e. imported), it will issue a warning to stderr if socks
# isn't available. requests unconditionally imports urllib3's socks contrib
# module, triggering this warning. The warning breaks DEP-8 tests (because of
# the stderr output) and is just plain annoying in normal usage. I don't want
# to add socks as yet another dependency for pip, nor do I want to allow-stder
# in the DEP-8 tests, so just suppress the warning. pdb tells me this has to
# be done before the import of pip9.vcs.
from pip9._vendor.urllib3.exceptions import DependencyWarning
warnings.filterwarnings("ignore", category=DependencyWarning) # noqa
# We want to inject the use of SecureTransport as early as possible so that any
# references or sessions or what have you are ensured to have it, however we
# only want to do this in the case that we're running on macOS and the linked
# OpenSSL is too old to handle TLSv1.2
try:
import ssl
except ImportError:
pass
else:
if (sys.platform == "darwin" and
getattr(ssl, "OPENSSL_VERSION_NUMBER", 0) < 0x1000100f): # OpenSSL 1.0.1
try:
from pip9._vendor.urllib3.contrib import securetransport
except (ImportError, OSError):
pass
else:
securetransport.inject_into_urllib3()
from pip9.exceptions import InstallationError, CommandError, PipError
from pip9.utils import get_installed_distributions, get_prog
from pip9.utils import deprecation, dist_is_editable
from pip9.vcs import git, mercurial, subversion, bazaar # noqa
from pip9.baseparser import ConfigOptionParser, UpdatingDefaultsHelpFormatter
from pip9.commands import get_summaries, get_similar_commands
from pip9.commands import commands_dict
from pip9._vendor.urllib3.exceptions import InsecureRequestWarning
# assignment for flake8 to be happy
# This fixes a peculiarity when importing via __import__ - as we are
# initialising the pip module, "from pip9.import cmdoptions" is recursive
# and appears not to work properly in that situation.
import pip9.cmdoptions
cmdoptions = pip9.cmdoptions
# The version as used in the setup.py and the docs conf.py
__version__ = "9.0.3"
logger = logging.getLogger(__name__)
# Hide the InsecureRequestWarning from urllib3
warnings.filterwarnings("ignore", category=InsecureRequestWarning)
def autocomplete():
"""Command and option completion for the main option parser (and options)
and its subcommands (and options).
Enable by sourcing one of the completion shell scripts (bash, zsh or fish).
"""
# Don't complete if user hasn't sourced bash_completion file.
if 'PIP_AUTO_COMPLETE' not in os.environ:
return
cwords = os.environ['COMP_WORDS'].split()[1:]
cword = int(os.environ['COMP_CWORD'])
try:
current = cwords[cword - 1]
except IndexError:
current = ''
subcommands = [cmd for cmd, summary in get_summaries()]
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 options
if subcommand_name:
# special case: 'help' subcommand has no options
if subcommand_name == 'help':
sys.exit(1)
# special case: list locally installed dists for uninstall command
if subcommand_name == 'uninstall' and not current.startswith('-'):
installed = []
lc = current.lower()
for dist in get_installed_distributions(local_only=True):
if dist.key.startswith(lc) and dist.key not in cwords[1:]:
installed.append(dist.key)
# if there are no dists installed, fall back to option completion
if installed:
for dist in installed:
print(dist)
sys.exit(1)
subcommand = commands_dict[subcommand_name]()
options += [(opt.get_opt_string(), opt.nargs)
for opt in subcommand.parser.option_list_all
if opt.help != optparse.SUPPRESS_HELP]
# filter out previously specified options from available options
prev_opts = [x.split('=')[0] for x in cwords[1:cword - 1]]
options = [(x, v) for (x, v) in options if x not in prev_opts]
# filter options by current input
options = [(k, v) for k, v in options if k.startswith(current)]
for option in options:
opt_label = option[0]
# append '=' to options which require args
if option[1]:
opt_label += '='
print(opt_label)
else:
# show main parser options only when necessary
if current.startswith('-') or current.startswith('--'):
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)
subcommands += [i.get_opt_string() for i in opts
if i.help != optparse.SUPPRESS_HELP]
print(' '.join([x for x in subcommands if x.startswith(current)]))
sys.exit(1)
def create_main_parser():
parser_kw = {
'usage': '\n%prog <command> [options]',
'add_help_option': False,
'formatter': UpdatingDefaultsHelpFormatter(),
'name': 'global',
'prog': get_prog(),
}
parser = ConfigOptionParser(**parser_kw)
parser.disable_interspersed_args()
pip_pkg_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
parser.version = 'pip %s from %s (python %s)' % (
__version__, pip_pkg_dir, sys.version[:3])
# add the general options
gen_opts = cmdoptions.make_option_group(cmdoptions.general_group, parser)
parser.add_option_group(gen_opts)
parser.main = True # so the help formatter knows
# create command listing for description
command_summaries = get_summaries()
description = [''] + ['%-27s %s' % (i, j) for i, j in command_summaries]
parser.description = '\n'.join(description)
return parser
def parseopts(args):
parser = create_main_parser()
# Note: parser calls disable_interspersed_args(), so the result of this
# call is to split the initial args into the general options before the
# subcommand and everything else.
# For example:
# args: ['--timeout=5', 'install', '--user', 'INITools']
# general_options: ['--timeout==5']
# args_else: ['install', '--user', 'INITools']
general_options, args_else = parser.parse_args(args)
# --version
if general_options.version:
sys.stdout.write(parser.version)
sys.stdout.write(os.linesep)
sys.exit()
# pip || pip help -> print_help()
if not args_else or (args_else[0] == 'help' and len(args_else) == 1):
parser.print_help()
sys.exit()
# the subcommand name
cmd_name = args_else[0]
if cmd_name not in commands_dict:
guess = get_similar_commands(cmd_name)
msg = ['unknown command "%s"' % cmd_name]
if guess:
msg.append('maybe you meant "%s"' % guess)
raise CommandError(' - '.join(msg))
# all the args without the subcommand
cmd_args = args[:]
cmd_args.remove(cmd_name)
return cmd_name, cmd_args
def check_isolated(args):
isolated = False
if "--isolated" in args:
isolated = True
return isolated
def main(args=None):
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 = parseopts(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 pip9.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 = commands_dict[cmd_name](isolated=check_isolated(cmd_args))
return command.main(cmd_args)
# ###########################################################
# # Writing freeze files
class FrozenRequirement(object):
def __init__(self, name, req, editable, comments=()):
self.name = name
self.req = req
self.editable = editable
self.comments = comments
_rev_re = re.compile(r'-r(\d+)$')
_date_re = re.compile(r'-(20\d\d\d\d\d\d)$')
@classmethod
def from_dist(cls, dist, dependency_links):
location = os.path.normcase(os.path.abspath(dist.location))
comments = []
from pip9.vcs import vcs, get_src_requirement
if dist_is_editable(dist) and vcs.get_backend_name(location):
editable = True
try:
req = get_src_requirement(dist, location)
except InstallationError as exc:
logger.warning(
"Error when trying to get requirement for VCS system %s, "
"falling back to uneditable format", exc
)
req = None
if req is None:
logger.warning(
'Could not determine repository location of %s', location
)
comments.append(
'## !! Could not determine repository location'
)
req = dist.as_requirement()
editable = False
else:
editable = False
req = dist.as_requirement()
specs = req.specs
assert len(specs) == 1 and specs[0][0] in ["==", "==="], \
'Expected 1 spec with == or ===; specs = %r; dist = %r' % \
(specs, dist)
version = specs[0][1]
ver_match = cls._rev_re.search(version)
date_match = cls._date_re.search(version)
if ver_match or date_match:
svn_backend = vcs.get_backend('svn')
if svn_backend:
svn_location = svn_backend().get_location(
dist,
dependency_links,
)
if not svn_location:
logger.warning(
'Warning: cannot find svn location for %s', req)
comments.append(
'## FIXME: could not find svn URL in dependency_links '
'for this package:'
)
else:
comments.append(
'# Installing as editable to satisfy requirement %s:' %
req
)
if ver_match:
rev = ver_match.group(1)
else:
rev = '{%s}' % date_match.group(1)
editable = True
req = '%s@%s#egg=%s' % (
svn_location,
rev,
cls.egg_name(dist)
)
return cls(dist.project_name, req, editable, comments)
@staticmethod
def egg_name(dist):
name = dist.egg_name()
match = re.search(r'-py\d\.\d$', name)
if match:
name = name[:match.start()]
return name
def __str__(self):
req = self.req
if self.editable:
req = '-e %s' % req
return '\n'.join(list(self.comments) + [str(req)]) + '\n'
if __name__ == '__main__':
sys.exit(main())
__version__ = "10.0.1"
+3 -3
View File
@@ -9,11 +9,11 @@ if __package__ == '':
# __file__ is pip-*.whl/pip/__main__.py
# first dirname call strips of '/__main__.py', second strips off '/pip'
# Resulting path is the name of the wheel itself
# Add that to sys.path so we can import pip9
# Add that to sys.path so we can import notpip
path = os.path.dirname(os.path.dirname(__file__))
sys.path.insert(0, path)
import pip9 # noqa
from notpip._internal import main as _main # noqa
if __name__ == '__main__':
sys.exit(pip9.main())
sys.exit(_main())
+246
View File
@@ -0,0 +1,246 @@
#!/usr/bin/env python
from __future__ import absolute_import
import locale
import logging
import os
import optparse
import warnings
import sys
# 2016-06-17 barry@debian.org: urllib3 1.14 added optional support for socks,
# but if invoked (i.e. imported), it will issue a warning to stderr if socks
# isn't available. requests unconditionally imports urllib3's socks contrib
# module, triggering this warning. The warning breaks DEP-8 tests (because of
# the stderr output) and is just plain annoying in normal usage. I don't want
# to add socks as yet another dependency for pip, nor do I want to allow-stder
# in the DEP-8 tests, so just suppress the warning. pdb tells me this has to
# be done before the import of pip.vcs.
from notpip._vendor.urllib3.exceptions import DependencyWarning
warnings.filterwarnings("ignore", category=DependencyWarning) # noqa
# We want to inject the use of SecureTransport as early as possible so that any
# references or sessions or what have you are ensured to have it, however we
# only want to do this in the case that we're running on macOS and the linked
# OpenSSL is too old to handle TLSv1.2
try:
import ssl
except ImportError:
pass
else:
# Checks for OpenSSL 1.0.1 on MacOS
if sys.platform == "darwin" and ssl.OPENSSL_VERSION_NUMBER < 0x1000100f:
try:
from notpip._vendor.urllib3.contrib import securetransport
except (ImportError, OSError):
pass
else:
securetransport.inject_into_urllib3()
from notpip import __version__
from notpip._internal import cmdoptions
from notpip._internal.exceptions import CommandError, PipError
from notpip._internal.utils.misc import get_installed_distributions, get_prog
from notpip._internal.utils import deprecation
from notpip._internal.vcs import git, mercurial, subversion, bazaar # noqa
from notpip._internal.baseparser import (
ConfigOptionParser, UpdatingDefaultsHelpFormatter,
)
from notpip._internal.commands import get_summaries, get_similar_commands
from notpip._internal.commands import commands_dict
from notpip._vendor.urllib3.exceptions import InsecureRequestWarning
logger = logging.getLogger(__name__)
# Hide the InsecureRequestWarning from urllib3
warnings.filterwarnings("ignore", category=InsecureRequestWarning)
def autocomplete():
"""Command and option completion for the main option parser (and options)
and its subcommands (and options).
Enable by sourcing one of the completion shell scripts (bash, zsh or fish).
"""
# Don't complete if user hasn't sourced bash_completion file.
if 'PIP_AUTO_COMPLETE' not in os.environ:
return
cwords = os.environ['COMP_WORDS'].split()[1:]
cword = int(os.environ['COMP_CWORD'])
try:
current = cwords[cword - 1]
except IndexError:
current = ''
subcommands = [cmd for cmd, summary in get_summaries()]
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 options
if subcommand_name:
# special case: 'help' subcommand has no options
if subcommand_name == 'help':
sys.exit(1)
# special case: list locally installed dists for show and uninstall
should_list_installed = (
subcommand_name in ['show', 'uninstall'] and
not current.startswith('-')
)
if should_list_installed:
installed = []
lc = current.lower()
for dist in get_installed_distributions(local_only=True):
if dist.key.startswith(lc) and dist.key not in cwords[1:]:
installed.append(dist.key)
# if there are no dists installed, fall back to option completion
if installed:
for dist in installed:
print(dist)
sys.exit(1)
subcommand = commands_dict[subcommand_name]()
for opt in subcommand.parser.option_list_all:
if opt.help != optparse.SUPPRESS_HELP:
for opt_str in opt._long_opts + opt._short_opts:
options.append((opt_str, opt.nargs))
# filter out previously specified options from available options
prev_opts = [x.split('=')[0] for x in cwords[1:cword - 1]]
options = [(x, v) for (x, v) in options if x not in prev_opts]
# filter options by current input
options = [(k, v) for k, v in options if k.startswith(current)]
for option in options:
opt_label = option[0]
# append '=' to options which require args
if option[1] and option[0][:2] == "--":
opt_label += '='
print(opt_label)
else:
# show main parser options only when necessary
if current.startswith('-') or current.startswith('--'):
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)
for opt in opts:
if opt.help != optparse.SUPPRESS_HELP:
subcommands += opt._long_opts + opt._short_opts
print(' '.join([x for x in subcommands if x.startswith(current)]))
sys.exit(1)
def create_main_parser():
parser_kw = {
'usage': '\n%prog <command> [options]',
'add_help_option': False,
'formatter': UpdatingDefaultsHelpFormatter(),
'name': 'global',
'prog': get_prog(),
}
parser = ConfigOptionParser(**parser_kw)
parser.disable_interspersed_args()
pip_pkg_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
parser.version = 'pip %s from %s (python %s)' % (
__version__, pip_pkg_dir, sys.version[:3],
)
# add the general options
gen_opts = cmdoptions.make_option_group(cmdoptions.general_group, parser)
parser.add_option_group(gen_opts)
parser.main = True # so the help formatter knows
# create command listing for description
command_summaries = get_summaries()
description = [''] + ['%-27s %s' % (i, j) for i, j in command_summaries]
parser.description = '\n'.join(description)
return parser
def parseopts(args):
parser = create_main_parser()
# Note: parser calls disable_interspersed_args(), so the result of this
# call is to split the initial args into the general options before the
# subcommand and everything else.
# For example:
# args: ['--timeout=5', 'install', '--user', 'INITools']
# general_options: ['--timeout==5']
# args_else: ['install', '--user', 'INITools']
general_options, args_else = parser.parse_args(args)
# --version
if general_options.version:
sys.stdout.write(parser.version)
sys.stdout.write(os.linesep)
sys.exit()
# pip || pip help -> print_help()
if not args_else or (args_else[0] == 'help' and len(args_else) == 1):
parser.print_help()
sys.exit()
# the subcommand name
cmd_name = args_else[0]
if cmd_name not in commands_dict:
guess = get_similar_commands(cmd_name)
msg = ['unknown command "%s"' % cmd_name]
if guess:
msg.append('maybe you meant "%s"' % guess)
raise CommandError(' - '.join(msg))
# all the args without the subcommand
cmd_args = args[:]
cmd_args.remove(cmd_name)
return cmd_name, cmd_args
def check_isolated(args):
isolated = False
if "--isolated" in args:
isolated = True
return isolated
def main(args=None):
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 = parseopts(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 = commands_dict[cmd_name](isolated=check_isolated(cmd_args))
return command.main(cmd_args)
@@ -2,40 +2,49 @@
from __future__ import absolute_import
import logging
import logging.config
import optparse
import os
import sys
import optparse
import warnings
from pip9 import cmdoptions
from pip9.index import PackageFinder
from pip9.locations import running_under_virtualenv
from pip9.download import pip9Session
from pip9.exceptions import (BadCommand, InstallationError, UninstallationError,
CommandError, PreviousBuildDirError)
from pip9.compat import logging_dictConfig
from pip9.baseparser import ConfigOptionParser, UpdatingDefaultsHelpFormatter
from pip9.req import InstallRequirement, parse_requirements
from pip9.status_codes import (
SUCCESS, ERROR, UNKNOWN_ERROR, VIRTUALENV_NOT_FOUND,
PREVIOUS_BUILD_DIR_ERROR,
from notpip._internal import cmdoptions
from notpip._internal.baseparser import (
ConfigOptionParser, UpdatingDefaultsHelpFormatter,
)
from pip9.utils import deprecation, get_prog, normalize_path
from pip9.utils.logging import IndentingFormatter
from pip9.utils.outdated import pip9_version_check
from notpip._internal.compat import WINDOWS
from notpip._internal.download import PipSession
from notpip._internal.exceptions import (
BadCommand, CommandError, InstallationError, PreviousBuildDirError,
UninstallationError,
)
from notpip._internal.index import PackageFinder
from notpip._internal.locations import running_under_virtualenv
from notpip._internal.req.req_file import parse_requirements
from notpip._internal.req.req_install import InstallRequirement
from notpip._internal.status_codes import (
ERROR, PREVIOUS_BUILD_DIR_ERROR, SUCCESS, UNKNOWN_ERROR,
VIRTUALENV_NOT_FOUND,
)
from notpip._internal.utils import deprecation
from notpip._internal.utils.logging import IndentingFormatter
from notpip._internal.utils.misc import get_prog, normalize_path
from notpip._internal.utils.outdated import pip_version_check
from notpip._internal.utils.typing import MYPY_CHECK_RUNNING
if MYPY_CHECK_RUNNING:
from typing import Optional
__all__ = ['Command']
logger = logging.getLogger(__name__)
class Command(object):
name = None
usage = None
hidden = False
name = None # type: Optional[str]
usage = None # type: Optional[str]
hidden = False # type: bool
ignore_require_venv = False # type: bool
log_streams = ("ext://sys.stdout", "ext://sys.stderr")
def __init__(self, isolated=False):
@@ -105,15 +114,17 @@ class Command(object):
def main(self, args):
options, args = self.parse_args(args)
if options.quiet:
if options.quiet == 1:
level = "WARNING"
if options.quiet == 2:
level = "ERROR"
else:
level = "CRITICAL"
elif options.verbose:
# Set verbosity so that it can be used elsewhere.
self.verbosity = options.verbose - options.quiet
if self.verbosity >= 1:
level = "DEBUG"
elif self.verbosity == -1:
level = "WARNING"
elif self.verbosity == -2:
level = "ERROR"
elif self.verbosity <= -3:
level = "CRITICAL"
else:
level = "INFO"
@@ -123,12 +134,15 @@ class Command(object):
if options.log:
root_level = "DEBUG"
logging_dictConfig({
logger_class = "pip._internal.utils.logging.ColorizedStreamHandler"
handler_class = "pip._internal.utils.logging.BetterRotatingFileHandler"
logging.config.dictConfig({
"version": 1,
"disable_existing_loggers": False,
"filters": {
"exclude_warnings": {
"()": "pip9.utils.logging.MaxLevelFilter",
"()": "pip._internal.utils.logging.MaxLevelFilter",
"level": logging.WARNING,
},
},
@@ -141,20 +155,22 @@ class Command(object):
"handlers": {
"console": {
"level": level,
"class": "pip9.utils.logging.ColorizedStreamHandler",
"class": logger_class,
"no_color": options.no_color,
"stream": self.log_streams[0],
"filters": ["exclude_warnings"],
"formatter": "indent",
},
"console_errors": {
"level": "WARNING",
"class": "pip9.utils.logging.ColorizedStreamHandler",
"class": logger_class,
"no_color": options.no_color,
"stream": self.log_streams[1],
"formatter": "indent",
},
"user_log": {
"level": "DEBUG",
"class": "pip9.utils.logging.BetterRotatingFileHandler",
"class": handler_class,
"filename": options.log or "/dev/null",
"delay": True,
"formatter": "indent",
@@ -169,29 +185,24 @@ class Command(object):
])),
},
# Disable any logging besides WARNING unless we have DEBUG level
# logging enabled. These use both pip9._vendor and the bare names
# logging enabled. These use both pip._vendor and the bare names
# for the case where someone unbundles our libraries.
"loggers": dict(
(
name,
{
"level": (
"WARNING"
if level in ["INFO", "ERROR"]
else "DEBUG"
),
},
)
for name in ["pip9._vendor", "distlib", "requests", "urllib3"]
),
"loggers": {
name: {
"level": (
"WARNING" if level in ["INFO", "ERROR"] else "DEBUG"
)
} for name in [
"pip._vendor", "distlib", "requests", "urllib3"
]
},
})
if sys.version_info[:2] == (2, 6):
if sys.version_info[:2] == (3, 3):
warnings.warn(
"Python 2.6 is no longer supported by the Python core team, "
"please upgrade your Python. A future version of pip will "
"drop support for Python 2.6",
deprecation.Python26DeprecationWarning
"Python 3.3 supported has been deprecated and support for it "
"will be dropped in the future. Please upgrade your Python.",
deprecation.RemovedInPip11Warning,
)
# TODO: try to get these passing down from the command?
@@ -203,7 +214,7 @@ class Command(object):
if options.exists_action:
os.environ['PIP_EXISTS_ACTION'] = ' '.join(options.exists_action)
if options.require_venv:
if options.require_venv and not self.ignore_require_venv:
# If a venv is required check if it can really be found
if not running_under_virtualenv():
logger.critical(
@@ -211,6 +222,8 @@ class Command(object):
)
sys.exit(VIRTUALENV_NOT_FOUND)
original_root_handlers = set(logging.root.handlers)
try:
status = self.run(options, args)
# FIXME: all commands should return an exit status
@@ -249,7 +262,11 @@ class Command(object):
options,
retries=0,
timeout=min(5, options.timeout)) as session:
pip_version_check(session)
pip_version_check(session, options)
# Avoid leaking loggers
for handler in set(logging.root.handlers) - original_root_handlers:
# this method benefit from the Logger class internal lock
logging.root.removeHandler(handler)
return SUCCESS
@@ -262,54 +279,73 @@ class RequirementCommand(Command):
"""
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 in parse_requirements(
for req_to_add in parse_requirements(
filename,
constraint=True, finder=finder, options=options,
session=session, wheel_cache=wheel_cache):
requirement_set.add_requirement(req)
req_to_add.is_direct = True
requirement_set.add_requirement(req_to_add)
for req in args:
requirement_set.add_requirement(
InstallRequirement.from_line(
req, None, isolated=options.isolated_mode,
wheel_cache=wheel_cache
)
req_to_add = InstallRequirement.from_line(
req, None, isolated=options.isolated_mode,
wheel_cache=wheel_cache
)
req_to_add.is_direct = True
requirement_set.add_requirement(req_to_add)
for req in options.editables:
requirement_set.add_requirement(
InstallRequirement.from_editable(
req,
default_vcs=options.default_vcs,
isolated=options.isolated_mode,
wheel_cache=wheel_cache
)
req_to_add = InstallRequirement.from_editable(
req,
isolated=options.isolated_mode,
wheel_cache=wheel_cache
)
req_to_add.is_direct = True
requirement_set.add_requirement(req_to_add)
found_req_in_file = False
for filename in options.requirements:
for req in parse_requirements(
for req_to_add in parse_requirements(
filename,
finder=finder, options=options, session=session,
wheel_cache=wheel_cache):
found_req_in_file = True
requirement_set.add_requirement(req)
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 not (args or options.editables or found_req_in_file):
if not (args or options.editables or options.requirements):
opts = {'name': name}
if options.find_links:
msg = ('You must give at least one requirement to '
'%(name)s (maybe you meant "pip %(name)s '
'%(links)s"?)' %
dict(opts, links=' '.join(options.find_links)))
raise CommandError(
'You must give at least one requirement to %(name)s '
'(maybe you meant "pip %(name)s %(links)s"?)' %
dict(opts, links=' '.join(options.find_links)))
else:
msg = ('You must give at least one requirement '
'to %(name)s (see "pip help %(name)s")' % opts)
logger.warning(msg)
raise CommandError(
'You must give at least one requirement to %(name)s '
'(see "pip help %(name)s")' % opts)
# On Windows, any operation modifying pip should be run as:
# python -m pip ...
# See https://github.com/pypa/pip/issues/1299 for more discussion
should_show_use_python_msg = (
WINDOWS and
requirement_set.has_requirement("pip") and
os.path.basename(sys.argv[0]).startswith("pip")
)
if should_show_use_python_msg:
new_command = [
sys.executable, "-m", "pip"
] + sys.argv[1:]
raise CommandError(
'To modify pip, please run the following command:\n{}'
.format(" ".join(new_command))
)
def _build_package_finder(self, options, session,
platform=None, python_versions=None,
@@ -1,23 +1,18 @@
"""Base option parser setup"""
from __future__ import absolute_import
import sys
import logging
import optparse
import os
import re
import sys
import textwrap
from distutils.util import strtobool
from pip9._vendor.six import string_types
from pip9._vendor.six.moves import configparser
from pip9.locations import (
legacy_config_file, config_basename, running_under_virtualenv,
site_config_files
)
from pip9.utils import appdirs, get_terminal_size
from notpip._vendor.six import string_types
from notpip._internal.compat import get_terminal_size
from notpip._internal.configuration import Configuration, ConfigurationError
_environ_prefix_re = re.compile(r"^PIP_", re.I)
logger = logging.getLogger(__name__)
class PrettyHelpFormatter(optparse.IndentedHelpFormatter):
@@ -137,58 +132,15 @@ class ConfigOptionParser(CustomOptionParser):
"""Custom option parser which updates its defaults by checking the
configuration files and environmental variables"""
isolated = False
def __init__(self, *args, **kwargs):
self.config = configparser.RawConfigParser()
self.name = kwargs.pop('name')
self.isolated = kwargs.pop("isolated", False)
self.files = self.get_config_files()
if self.files:
self.config.read(self.files)
isolated = kwargs.pop("isolated", False)
self.config = Configuration(isolated)
assert self.name
optparse.OptionParser.__init__(self, *args, **kwargs)
def get_config_files(self):
# the files returned by this method will be parsed in order with the
# first files listed being overridden by later files in standard
# ConfigParser fashion
config_file = os.environ.get('PIP_CONFIG_FILE', False)
if config_file == os.devnull:
return []
# at the base we have any site-wide configuration
files = list(site_config_files)
# per-user configuration next
if not self.isolated:
if config_file and os.path.exists(config_file):
files.append(config_file)
else:
# This is the legacy config file, we consider it to be a lower
# priority than the new file location.
files.append(legacy_config_file)
# This is the new config file, we consider it to be a higher
# priority than the legacy file.
files.append(
os.path.join(
appdirs.user_config_dir("pip"),
config_basename,
)
)
# finally virtualenv configuration first trumping others
if running_under_virtualenv():
venv_config_file = os.path.join(
sys.prefix,
config_basename,
)
if os.path.exists(venv_config_file):
files.append(venv_config_file)
return files
def check_default(self, option, key, val):
try:
return option.check_value(key, val)
@@ -196,30 +148,43 @@ class ConfigOptionParser(CustomOptionParser):
print("An error occurred during configuration: %s" % exc)
sys.exit(3)
def _get_ordered_configuration_items(self):
# Configuration gives keys in an unordered manner. Order them.
override_order = ["global", self.name, ":env:"]
# Pool the options into different groups
section_items = {name: [] for name in override_order}
for section_key, val in self.config.items():
# ignore empty values
if not val:
logger.debug(
"Ignoring configuration key '%s' as it's value is empty.",
section_key
)
continue
section, key = section_key.split(".", 1)
if section in override_order:
section_items[section].append((key, val))
# Yield each group in their override order
for section in override_order:
for key, val in section_items[section]:
yield key, val
def _update_defaults(self, defaults):
"""Updates the given defaults with values from the config files and
the environ. Does a little special handling for certain types of
options (lists)."""
# Then go and look for the other sources of configuration:
config = {}
# 1. config files
for section in ('global', self.name):
config.update(
self.normalize_keys(self.get_config_section(section))
)
# 2. environmental variables
if not self.isolated:
config.update(self.normalize_keys(self.get_environ_vars()))
# Accumulate complex default state.
self.values = optparse.Values(self.defaults)
late_eval = set()
# Then set the options with those values
for key, val in config.items():
# ignore empty values
if not val:
continue
for key, val in self._get_ordered_configuration_items():
# '--' because configuration supports only long names
option = self.get_option('--' + key)
option = self.get_option(key)
# Ignore options not present in this parser. E.g. non-globals put
# in [global] by users that want them to apply to all applicable
# commands.
@@ -249,30 +214,6 @@ class ConfigOptionParser(CustomOptionParser):
self.values = None
return defaults
def normalize_keys(self, items):
"""Return a config dictionary with normalized keys regardless of
whether the keys were specified in environment variables or in config
files"""
normalized = {}
for key, val in items:
key = key.replace('_', '-')
if not key.startswith('--'):
key = '--%s' % key # only prefer long opts
normalized[key] = val
return normalized
def get_config_section(self, name):
"""Get a section of a configuration"""
if self.config.has_section(name):
return self.config.items(name)
return []
def get_environ_vars(self):
"""Returns a generator with all environmental vars with prefix PIP_"""
for key, val in os.environ.items():
if _environ_prefix_re.search(key):
yield (_environ_prefix_re.sub("", key).lower(), val)
def get_default_values(self):
"""Overriding to make updating the defaults after instantiation of
the option parser possible, _update_defaults() does the dirty work."""
@@ -280,6 +221,12 @@ class ConfigOptionParser(CustomOptionParser):
# Old, pre-Optik 1.5 behaviour.
return optparse.Values(self.defaults)
# Load the configuration, or error out in case of an error
try:
self.config.load()
except ConfigurationError as err:
self.exit(2, err.args[0])
defaults = self._update_defaults(self.defaults.copy()) # ours
for option in self._get_all_options():
default = defaults.get(option.dest)
@@ -0,0 +1,92 @@
"""Build Environment used for isolation during sdist building
"""
import os
from distutils.sysconfig import get_python_lib
from sysconfig import get_paths
from notpip._internal.utils.temp_dir import TempDirectory
class BuildEnvironment(object):
"""Creates and manages an isolated environment to install build deps
"""
def __init__(self, no_clean):
self._temp_dir = TempDirectory(kind="build-env")
self._no_clean = no_clean
@property
def path(self):
return self._temp_dir.path
def __enter__(self):
self._temp_dir.create()
self.save_path = os.environ.get('PATH', None)
self.save_pythonpath = os.environ.get('PYTHONPATH', None)
self.save_nousersite = os.environ.get('PYTHONNOUSERSITE', None)
install_scheme = 'nt' if (os.name == 'nt') else 'posix_prefix'
install_dirs = get_paths(install_scheme, vars={
'base': self.path,
'platbase': self.path,
})
scripts = install_dirs['scripts']
if self.save_path:
os.environ['PATH'] = scripts + os.pathsep + self.save_path
else:
os.environ['PATH'] = scripts + os.pathsep + os.defpath
# Note: prefer distutils' sysconfig to get the
# library paths so PyPy is correctly supported.
purelib = get_python_lib(plat_specific=0, prefix=self.path)
platlib = get_python_lib(plat_specific=1, prefix=self.path)
if purelib == platlib:
lib_dirs = purelib
else:
lib_dirs = purelib + os.pathsep + platlib
if self.save_pythonpath:
os.environ['PYTHONPATH'] = lib_dirs + os.pathsep + \
self.save_pythonpath
else:
os.environ['PYTHONPATH'] = lib_dirs
os.environ['PYTHONNOUSERSITE'] = '1'
return self.path
def __exit__(self, exc_type, exc_val, exc_tb):
if not self._no_clean:
self._temp_dir.cleanup()
def restore_var(varname, old_value):
if old_value is None:
os.environ.pop(varname, None)
else:
os.environ[varname] = old_value
restore_var('PATH', self.save_path)
restore_var('PYTHONPATH', self.save_pythonpath)
restore_var('PYTHONNOUSERSITE', self.save_nousersite)
def cleanup(self):
self._temp_dir.cleanup()
class NoOpBuildEnvironment(BuildEnvironment):
"""A no-op drop-in replacement for BuildEnvironment
"""
def __init__(self, no_clean):
pass
def __enter__(self):
pass
def __exit__(self, exc_type, exc_val, exc_tb):
pass
def cleanup(self):
pass
+202
View File
@@ -0,0 +1,202 @@
"""Cache Management
"""
import errno
import hashlib
import logging
import os
from notpip._vendor.packaging.utils import canonicalize_name
from notpip._internal import index
from notpip._internal.compat import expanduser
from notpip._internal.download import path_to_url
from notpip._internal.utils.temp_dir import TempDirectory
from notpip._internal.wheel import InvalidWheelFilename, Wheel
logger = logging.getLogger(__name__)
class Cache(object):
"""An abstract class - provides cache directories for data from links
:param cache_dir: The root of the cache.
:param format_control: A pip.index.FormatControl object to limit
binaries being read from the cache.
:param allowed_formats: which formats of files the cache should store.
('binary' and 'source' are the only allowed values)
"""
def __init__(self, cache_dir, format_control, allowed_formats):
super(Cache, self).__init__()
self.cache_dir = expanduser(cache_dir) if cache_dir else 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):
"""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 = [link.url_without_fragment]
if link.hash_name is not None and link.hash is not None:
key_parts.append("=".join([link.hash_name, link.hash]))
key_url = "#".join(key_parts)
# 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 = hashlib.sha224(key_url.encode()).hexdigest()
# 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, package_name):
can_not_cache = (
not self.cache_dir or
not package_name or
not link
)
if can_not_cache:
return []
canonical_name = canonicalize_name(package_name)
formats = index.fmt_ctl_formats(
self.format_control, canonical_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
def get_path_for_link(self, link):
"""Return a directory to store cached items in for link.
"""
raise NotImplementedError()
def get(self, link, package_name):
"""Returns a link to a cached item if it exists, otherwise returns the
passed link.
"""
raise NotImplementedError()
def _link_for_candidate(self, link, candidate):
root = self.get_path_for_link(link)
path = os.path.join(root, candidate)
return index.Link(path_to_url(path))
def cleanup(self):
pass
class SimpleWheelCache(Cache):
"""A cache of wheels for future installs.
"""
def __init__(self, cache_dir, format_control):
super(SimpleWheelCache, self).__init__(
cache_dir, format_control, {"binary"}
)
def get_path_for_link(self, link):
"""Return a directory to store cached wheels for link
Because there are M wheels for any one sdist, we provide a directory
to cache them in, and then consult that directory when looking up
cache hits.
We only insert things into the cache if they have plausible version
numbers, so that we don't contaminate the cache with things that were
not unique. E.g. ./package might have dozens of installs done for it
and build a version of 0.0...and if we built and cached a wheel, we'd
end up using the same wheel even if the source has been edited.
:param link: The link of the sdist for which this will cache wheels.
"""
parts = self._get_cache_path_parts(link)
# Store wheels within the root cache_dir
return os.path.join(self.cache_dir, "wheels", *parts)
def get(self, link, package_name):
candidates = []
for wheel_name in self._get_candidates(link, package_name):
try:
wheel = Wheel(wheel_name)
except InvalidWheelFilename:
continue
if not wheel.supported():
# Built for a different python/arch/etc
continue
candidates.append((wheel.support_index_min(), wheel_name))
if not candidates:
return link
return self._link_for_candidate(link, min(candidates)[1])
class EphemWheelCache(SimpleWheelCache):
"""A SimpleWheelCache that creates it's own temporary cache directory
"""
def __init__(self, format_control):
self._temp_dir = TempDirectory(kind="ephem-wheel-cache")
self._temp_dir.create()
super(EphemWheelCache, self).__init__(
self._temp_dir.path, format_control
)
def cleanup(self):
self._temp_dir.cleanup()
class WheelCache(Cache):
"""Wraps EphemWheelCache and SimpleWheelCache into a single Cache
This Cache allows for gracefully degradation, using the ephem wheel cache
when a certain link is not found in the simple wheel cache first.
"""
def __init__(self, cache_dir, format_control):
super(WheelCache, self).__init__(
cache_dir, format_control, {'binary'}
)
self._wheel_cache = SimpleWheelCache(cache_dir, format_control)
self._ephem_cache = EphemWheelCache(format_control)
def get_path_for_link(self, link):
return self._wheel_cache.get_path_for_link(link)
def get_ephem_path_for_link(self, link):
return self._ephem_cache.get_path_for_link(link)
def get(self, link, package_name):
retval = self._wheel_cache.get(link, package_name)
if retval is link:
retval = self._ephem_cache.get(link, package_name)
return retval
def cleanup(self):
self._wheel_cache.cleanup()
self._ephem_cache.cleanup()
@@ -9,16 +9,21 @@ pass on state. To be consistent, all options will follow this design.
"""
from __future__ import absolute_import
from functools import partial
from optparse import OptionGroup, SUPPRESS_HELP, Option
import warnings
from functools import partial
from optparse import SUPPRESS_HELP, Option, OptionGroup
from pip9.index import (
from notpip._internal.index import (
FormatControl, fmt_ctl_handle_mutual_exclude, fmt_ctl_no_binary,
fmt_ctl_no_use_wheel)
from pip9.models import PyPI
from pip9.locations import USER_CACHE_DIR, src_prefix
from pip9.utils.hashes import STRONG_HASHES
)
from notpip._internal.locations import USER_CACHE_DIR, src_prefix
from notpip._internal.models import PyPI
from notpip._internal.utils.hashes import STRONG_HASHES
from notpip._internal.utils.typing import MYPY_CHECK_RUNNING
from notpip._internal.utils.ui import BAR_TYPES
if MYPY_CHECK_RUNNING:
from typing import Any
def make_option_group(group, parser):
@@ -33,12 +38,6 @@ def make_option_group(group, parser):
return option_group
def resolve_wheel_no_use_binary(options):
if not options.use_wheel:
control = options.format_control
fmt_ctl_no_use_wheel(control)
def check_install_build_global(options, check_options=None):
"""Disable wheels if per-setup.py call options are set.
@@ -57,7 +56,8 @@ def check_install_build_global(options, check_options=None):
fmt_ctl_no_binary(control)
warnings.warn(
'Disabling all use of wheels due to the use of --build-options '
'/ --global-options / --install-options.', stacklevel=2)
'/ --global-options / --install-options.', stacklevel=2,
)
###########
@@ -69,7 +69,8 @@ help_ = partial(
'-h', '--help',
dest='help',
action='help',
help='Show help.')
help='Show help.',
) # type: Any
isolated_mode = partial(
Option,
@@ -90,7 +91,8 @@ require_virtualenv = partial(
dest='require_venv',
action='store_true',
default=False,
help=SUPPRESS_HELP)
help=SUPPRESS_HELP
) # type: Any
verbose = partial(
Option,
@@ -101,12 +103,22 @@ verbose = partial(
help='Give more output. Option is additive, and can be used up to 3 times.'
)
no_color = partial(
Option,
'--no-color',
dest='no_color',
action='store_true',
default=False,
help="Suppress colored output",
)
version = partial(
Option,
'-V', '--version',
dest='version',
action='store_true',
help='Show version and exit.')
help='Show version and exit.',
) # type: Any
quiet = partial(
Option,
@@ -114,10 +126,25 @@ quiet = partial(
dest='quiet',
action='count',
default=0,
help=('Give less output. Option is additive, and can be used up to 3'
' times (corresponding to WARNING, ERROR, and CRITICAL logging'
' levels).')
)
help=(
'Give less output. Option is additive, and can be used up to 3'
' times (corresponding to WARNING, ERROR, and CRITICAL logging'
' levels).'
),
) # type: Any
progress_bar = partial(
Option,
'--progress-bar',
dest='progress_bar',
type='choice',
choices=list(BAR_TYPES.keys()),
default='on',
help=(
'Specify type of progress to be displayed [' +
'|'.join(BAR_TYPES.keys()) + '] (default: %default)'
),
) # type: Any
log = partial(
Option,
@@ -125,7 +152,7 @@ log = partial(
dest="log",
metavar="path",
help="Path to a verbose appending log."
)
) # type: Any
no_input = partial(
Option,
@@ -134,7 +161,8 @@ no_input = partial(
dest='no_input',
action='store_true',
default=False,
help=SUPPRESS_HELP)
help=SUPPRESS_HELP
) # type: Any
proxy = partial(
Option,
@@ -142,7 +170,8 @@ proxy = partial(
dest='proxy',
type='str',
default='',
help="Specify a proxy in the form [user:passwd@]proxy.server:port.")
help="Specify a proxy in the form [user:passwd@]proxy.server:port."
) # type: Any
retries = partial(
Option,
@@ -151,7 +180,8 @@ retries = partial(
type='int',
default=5,
help="Maximum number of retries each connection should attempt "
"(default %default times).")
"(default %default times).",
) # type: Any
timeout = partial(
Option,
@@ -160,16 +190,8 @@ timeout = partial(
dest='timeout',
type='float',
default=15,
help='Set the socket timeout (default %default seconds).')
default_vcs = partial(
Option,
# The default version control system for editables, e.g. 'svn'
'--default-vcs',
dest='default_vcs',
type='str',
default='',
help=SUPPRESS_HELP)
help='Set the socket timeout (default %default seconds).',
) # type: Any
skip_requirements_regex = partial(
Option,
@@ -178,7 +200,8 @@ skip_requirements_regex = partial(
dest='skip_requirements_regex',
type='str',
default='',
help=SUPPRESS_HELP)
help=SUPPRESS_HELP,
) # type: Any
def exists_action():
@@ -192,7 +215,8 @@ def exists_action():
action='append',
metavar='action',
help="Default action when a path already exists: "
"(s)witch, (i)gnore, (w)ipe, (b)ackup, (a)bort.")
"(s)witch, (i)gnore, (w)ipe, (b)ackup, (a)bort).",
)
cert = partial(
@@ -201,7 +225,8 @@ cert = partial(
dest='cert',
type='str',
metavar='path',
help="Path to alternate CA bundle.")
help="Path to alternate CA bundle.",
) # type: Any
client_cert = partial(
Option,
@@ -211,7 +236,8 @@ client_cert = partial(
default=None,
metavar='path',
help="Path to SSL client certificate, a single file containing the "
"private key and the certificate in PEM format.")
"private key and the certificate in PEM format.",
) # type: Any
index_url = partial(
Option,
@@ -222,7 +248,8 @@ index_url = partial(
help="Base URL of Python Package Index (default %default). "
"This should point to a repository compliant with PEP 503 "
"(the simple repository API) or a local directory laid out "
"in the same format.")
"in the same format.",
) # type: Any
def extra_index_url():
@@ -234,7 +261,7 @@ def extra_index_url():
default=[],
help="Extra URLs of package indexes to use in addition to "
"--index-url. Should follow the same rules as "
"--index-url."
"--index-url.",
)
@@ -244,7 +271,8 @@ no_index = partial(
dest='no_index',
action='store_true',
default=False,
help='Ignore package index (only looking at --find-links URLs instead).')
help='Ignore package index (only looking at --find-links URLs instead).',
) # type: Any
def find_links():
@@ -256,30 +284,10 @@ def find_links():
metavar='url',
help="If a url or path to an html file, then parse for links to "
"archives. If a local path or file:// url that's a directory, "
"then look for archives in the directory listing.")
def allow_external():
return Option(
"--allow-external",
dest="allow_external",
action="append",
default=[],
metavar="PACKAGE",
help=SUPPRESS_HELP,
"then look for archives in the directory listing.",
)
allow_all_external = partial(
Option,
"--allow-all-external",
dest="allow_all_external",
action="store_true",
default=False,
help=SUPPRESS_HELP,
)
def trusted_host():
return Option(
"--trusted-host",
@@ -292,38 +300,6 @@ def trusted_host():
)
# Remove after 7.0
no_allow_external = partial(
Option,
"--no-allow-external",
dest="allow_all_external",
action="store_false",
default=False,
help=SUPPRESS_HELP,
)
# Remove --allow-insecure after 7.0
def allow_unsafe():
return Option(
"--allow-unverified", "--allow-insecure",
dest="allow_unverified",
action="append",
default=[],
metavar="PACKAGE",
help=SUPPRESS_HELP,
)
# Remove after 7.0
no_allow_unsafe = partial(
Option,
"--no-allow-insecure",
dest="allow_all_insecure",
action="store_false",
default=False,
help=SUPPRESS_HELP
)
# Remove after 1.5
process_dependency_links = partial(
Option,
@@ -332,7 +308,7 @@ process_dependency_links = partial(
action="store_true",
default=False,
help="Enable the processing of dependency links.",
)
) # type: Any
def constraints():
@@ -343,7 +319,8 @@ def constraints():
default=[],
metavar='file',
help='Constrain versions using the given constraints file. '
'This option can be used multiple times.')
'This option can be used multiple times.'
)
def requirements():
@@ -354,7 +331,8 @@ def requirements():
default=[],
metavar='file',
help='Install from the given requirements file. '
'This option can be used multiple times.')
'This option can be used multiple times.'
)
def editable():
@@ -368,6 +346,7 @@ def editable():
'"develop mode") from a local project path or a VCS url.'),
)
src = partial(
Option,
'--src', '--source', '--source-dir', '--source-directory',
@@ -377,28 +356,7 @@ src = partial(
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".'
)
# XXX: deprecated, remove in 9.0
use_wheel = partial(
Option,
'--use-wheel',
dest='use_wheel',
action='store_true',
default=True,
help=SUPPRESS_HELP,
)
# XXX: deprecated, remove in 9.0
no_use_wheel = partial(
Option,
'--no-use-wheel',
dest='use_wheel',
action='store_false',
default=True,
help=('Do not Find and prefer wheel archives when searching indexes and '
'find-links locations. DEPRECATED in favour of --no-binary.'),
)
) # type: Any
def _get_format_control(values, option):
@@ -409,13 +367,15 @@ def _get_format_control(values, option):
def _handle_no_binary(option, opt_str, value, parser):
existing = getattr(parser.values, option.dest)
fmt_ctl_handle_mutual_exclude(
value, existing.no_binary, existing.only_binary)
value, existing.no_binary, existing.only_binary,
)
def _handle_only_binary(option, opt_str, value, parser):
existing = getattr(parser.values, option.dest)
fmt_ctl_handle_mutual_exclude(
value, existing.only_binary, existing.no_binary)
value, existing.only_binary, existing.no_binary,
)
def no_binary():
@@ -428,7 +388,8 @@ def no_binary():
"disable all binary packages, :none: to empty the set, or one or "
"more package names with commas between them. Note that some "
"packages are tricky to compile and may fail to install when "
"this option is used on them.")
"this option is used on them.",
)
def only_binary():
@@ -441,7 +402,8 @@ def only_binary():
"disable all source packages, :none: to empty the set, or one or "
"more package names with commas between them. Packages without "
"binary distributions will fail to install when this option is "
"used on them.")
"used on them.",
)
cache_dir = partial(
@@ -467,22 +429,39 @@ no_deps = partial(
dest='ignore_dependencies',
action='store_true',
default=False,
help="Don't install package dependencies.")
help="Don't install package dependencies.",
) # type: Any
build_dir = partial(
Option,
'-b', '--build', '--build-dir', '--build-directory',
dest='build_dir',
metavar='dir',
help='Directory to unpack packages into and build in.'
)
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 '
'the TMPDIR environment variable (TEMP on Windows) appropriately. '
'When passed, build directories are not cleaned in case of failures.'
) # type: Any
ignore_requires_python = partial(
Option,
'--ignore-requires-python',
dest='ignore_requires_python',
action='store_true',
help='Ignore the Requires-Python information.')
help='Ignore the Requires-Python information.'
) # type: Any
no_build_isolation = partial(
Option,
'--no-build-isolation',
dest='build_isolation',
action='store_false',
default=True,
help='Disable isolation when building a modern source distribution. '
'Build dependencies specified by PEP 518 must be already installed '
'if this option is used.'
) # type: Any
install_options = partial(
Option,
@@ -494,7 +473,8 @@ install_options = partial(
"command (use like --install-option=\"--install-scripts=/usr/local/"
"bin\"). Use multiple --install-option options to pass multiple "
"options to setup.py install. If you are using an option with a "
"directory path, be sure to use absolute path.")
"directory path, be sure to use absolute path.",
) # type: Any
global_options = partial(
Option,
@@ -503,14 +483,16 @@ global_options = partial(
action='append',
metavar='options',
help="Extra global options to be supplied to the setup.py "
"call before the install command.")
"call before the install command.",
) # type: Any
no_clean = partial(
Option,
'--no-clean',
action='store_true',
default=False,
help="Don't clean up build directories.")
help="Don't clean up build directories)."
) # type: Any
pre = partial(
Option,
@@ -518,7 +500,8 @@ pre = partial(
action='store_true',
default=False,
help="Include pre-release and development versions. By default, "
"pip only finds stable versions.")
"pip only finds stable versions.",
) # type: Any
disable_pip_version_check = partial(
Option,
@@ -527,7 +510,9 @@ disable_pip_version_check = partial(
action="store_true",
default=False,
help="Don't periodically check PyPI to determine whether a new version "
"of pip is available for download. Implied with --no-index.")
"of pip is available for download. Implied with --no-index.",
) # type: Any
# Deprecated, Remove later
always_unzip = partial(
@@ -536,7 +521,7 @@ always_unzip = partial(
dest='always_unzip',
action='store_true',
help=SUPPRESS_HELP,
)
) # type: Any
def _merge_hash(option, opt_str, value, parser):
@@ -566,7 +551,8 @@ hash = partial(
callback=_merge_hash,
type='string',
help="Verify that the package's archive matches this "
'hash before installing. Example: --hash=sha256:abcdef...')
'hash before installing. Example: --hash=sha256:abcdef...',
) # type: Any
require_hashes = partial(
@@ -577,7 +563,8 @@ require_hashes = partial(
default=False,
help='Require a hash to check each requirement against, for '
'repeatable installs. This option is implied when any package in a '
'requirements file has a --hash option.')
'requirements file has a --hash option.',
) # type: Any
##########
@@ -598,7 +585,6 @@ general_group = {
proxy,
retries,
timeout,
default_vcs,
skip_requirements_regex,
exists_action,
trusted_host,
@@ -607,10 +593,11 @@ general_group = {
cache_dir,
no_cache,
disable_pip_version_check,
no_color,
]
}
non_deprecated_index_group = {
index_group = {
'name': 'Package Index Options',
'options': [
index_url,
@@ -620,14 +607,3 @@ non_deprecated_index_group = {
process_dependency_links,
]
}
index_group = {
'name': 'Package Index Options (including deprecated options)',
'options': non_deprecated_index_group['options'] + [
allow_external,
allow_all_external,
no_allow_external,
allow_unsafe,
no_allow_unsafe,
]
}
@@ -3,35 +3,25 @@ Package containing all pip commands
"""
from __future__ import absolute_import
from pip9.commands.completion import CompletionCommand
from pip9.commands.download import DownloadCommand
from pip9.commands.freeze import FreezeCommand
from pip9.commands.hash import HashCommand
from pip9.commands.help import HelpCommand
from pip9.commands.list import ListCommand
from pip9.commands.check import CheckCommand
from pip9.commands.search import SearchCommand
from pip9.commands.show import ShowCommand
from pip9.commands.install import InstallCommand
from pip9.commands.uninstall import UninstallCommand
from pip9.commands.wheel import WheelCommand
from notpip._internal.commands.completion import CompletionCommand
from notpip._internal.commands.configuration import ConfigurationCommand
from notpip._internal.commands.download import DownloadCommand
from notpip._internal.commands.freeze import FreezeCommand
from notpip._internal.commands.hash import HashCommand
from notpip._internal.commands.help import HelpCommand
from notpip._internal.commands.list import ListCommand
from notpip._internal.commands.check import CheckCommand
from notpip._internal.commands.search import SearchCommand
from notpip._internal.commands.show import ShowCommand
from notpip._internal.commands.install import InstallCommand
from notpip._internal.commands.uninstall import UninstallCommand
from notpip._internal.commands.wheel import WheelCommand
from notpip._internal.utils.typing import MYPY_CHECK_RUNNING
commands_dict = {
CompletionCommand.name: CompletionCommand,
FreezeCommand.name: FreezeCommand,
HashCommand.name: HashCommand,
HelpCommand.name: HelpCommand,
SearchCommand.name: SearchCommand,
ShowCommand.name: ShowCommand,
InstallCommand.name: InstallCommand,
UninstallCommand.name: UninstallCommand,
DownloadCommand.name: DownloadCommand,
ListCommand.name: ListCommand,
CheckCommand.name: CheckCommand,
WheelCommand.name: WheelCommand,
}
if MYPY_CHECK_RUNNING:
from typing import List, Type
from notpip._internal.basecommand import Command
commands_order = [
InstallCommand,
@@ -41,12 +31,15 @@ commands_order = [
ListCommand,
ShowCommand,
CheckCommand,
ConfigurationCommand,
SearchCommand,
WheelCommand,
HashCommand,
CompletionCommand,
HelpCommand,
]
] # type: List[Type[Command]]
commands_dict = {c.name: c for c in commands_order}
def get_summaries(ordered=True):
@@ -0,0 +1,42 @@
import logging
from notpip._internal.basecommand import Command
from notpip._internal.operations.check import (
check_package_set, create_package_set_from_installed,
)
from notpip._internal.utils.misc import get_installed_distributions
logger = logging.getLogger(__name__)
class CheckCommand(Command):
"""Verify installed packages have compatible dependencies."""
name = 'check'
usage = """
%prog [options]"""
summary = 'Verify installed packages have compatible dependencies.'
def run(self, options, args):
package_set = create_package_set_from_installed()
missing, conflicting = check_package_set(package_set)
for project_name in missing:
version = package_set[project_name].version
for dependency in missing[project_name]:
logger.info(
"%s %s requires %s, which is not installed.",
project_name, version, dependency[0],
)
for project_name in conflicting:
version = package_set[project_name].version
for dep_name, dep_version, req in conflicting[project_name]:
logger.info(
"%s %s has requirement %s, but you have %s %s.",
project_name, version, req, dep_name, dep_version,
)
if missing or conflicting:
return 1
else:
logger.info("No broken requirements found.")
@@ -1,7 +1,10 @@
from __future__ import absolute_import
import sys
from pip9.basecommand import Command
import textwrap
from notpip._internal.basecommand import Command
from notpip._internal.utils.misc import get_prog
BASE_COMPLETION = """
# pip %(shell)s completion start%(script)s# pip %(shell)s completion end
@@ -9,38 +12,44 @@ BASE_COMPLETION = """
COMPLETION_SCRIPTS = {
'bash': """
_pip_completion()
{
COMPREPLY=( $( COMP_WORDS="${COMP_WORDS[*]}" \\
COMP_CWORD=$COMP_CWORD \\
PIP_AUTO_COMPLETE=1 $1 ) )
_pip_completion()
{
COMPREPLY=( $( COMP_WORDS="${COMP_WORDS[*]}" \\
COMP_CWORD=$COMP_CWORD \\
PIP_AUTO_COMPLETE=1 $1 ) )
}
complete -o default -F _pip_completion %(prog)s
""",
'zsh': """
function _pip_completion {
local words cword
read -Ac words
read -cn cword
reply=( $( COMP_WORDS="$words[*]" \\
COMP_CWORD=$(( cword-1 )) \\
PIP_AUTO_COMPLETE=1 $words[1] ) )
}
compctl -K _pip_completion %(prog)s
""",
'fish': """
function __fish_complete_pip
set -lx COMP_WORDS (commandline -o) ""
set -lx COMP_CWORD ( \\
math (contains -i -- (commandline -t) $COMP_WORDS)-1 \\
)
set -lx PIP_AUTO_COMPLETE 1
string split \\ -- (eval $COMP_WORDS[1])
end
complete -fa "(__fish_complete_pip)" -c %(prog)s
""",
}
complete -o default -F _pip_completion pip
""", 'zsh': """
function _pip_completion {
local words cword
read -Ac words
read -cn cword
reply=( $( COMP_WORDS="$words[*]" \\
COMP_CWORD=$(( cword-1 )) \\
PIP_AUTO_COMPLETE=1 $words[1] ) )
}
compctl -K _pip_completion pip
""", 'fish': """
function __fish_complete_pip
set -lx COMP_WORDS (commandline -o) ""
set -lx COMP_CWORD (math (contains -i -- (commandline -t) $COMP_WORDS)-1)
set -lx PIP_AUTO_COMPLETE 1
string split \ -- (eval $COMP_WORDS[1])
end
complete -fa "(__fish_complete_pip)" -c pip
"""}
class CompletionCommand(Command):
"""A helper command to be used for command completion."""
name = 'completion'
summary = 'A helper command used for command completion.'
ignore_require_venv = True
def __init__(self, *args, **kw):
super(CompletionCommand, self).__init__(*args, **kw)
@@ -73,7 +82,11 @@ class CompletionCommand(Command):
shells = COMPLETION_SCRIPTS.keys()
shell_options = ['--' + shell for shell in sorted(shells)]
if options.shell in shells:
script = COMPLETION_SCRIPTS.get(options.shell, '')
script = textwrap.dedent(
COMPLETION_SCRIPTS.get(options.shell, '') % {
'prog': get_prog(),
}
)
print(BASE_COMPLETION % {'script': script, 'shell': options.shell})
else:
sys.stderr.write(
@@ -0,0 +1,227 @@
import logging
import os
import subprocess
from notpip._internal.basecommand import Command
from notpip._internal.configuration import Configuration, kinds
from notpip._internal.exceptions import PipError
from notpip._internal.locations import venv_config_file
from notpip._internal.status_codes import ERROR, SUCCESS
from notpip._internal.utils.misc import get_prog
logger = logging.getLogger(__name__)
class ConfigurationCommand(Command):
"""Manage local and global configuration.
Subcommands:
list: List the active configuration (or from the file specified)
edit: Edit the configuration file in an editor
get: Get the value associated with name
set: Set the name=value
unset: Unset the value associated with name
If none of --user, --global and --venv are passed, a virtual
environment configuration file is used if one is active and the file
exists. Otherwise, all modifications happen on the to the user file by
default.
"""
name = 'config'
usage = """
%prog [<file-option>] list
%prog [<file-option>] [--editor <editor-path>] edit
%prog [<file-option>] get name
%prog [<file-option>] set name value
%prog [<file-option>] unset name
"""
summary = "Manage local and global configuration."
def __init__(self, *args, **kwargs):
super(ConfigurationCommand, self).__init__(*args, **kwargs)
self.configuration = None
self.cmd_opts.add_option(
'--editor',
dest='editor',
action='store',
default=None,
help=(
'Editor to use to edit the file. Uses VISUAL or EDITOR '
'environment variables if not provided.'
)
)
self.cmd_opts.add_option(
'--global',
dest='global_file',
action='store_true',
default=False,
help='Use the system-wide configuration file only'
)
self.cmd_opts.add_option(
'--user',
dest='user_file',
action='store_true',
default=False,
help='Use the user configuration file only'
)
self.cmd_opts.add_option(
'--venv',
dest='venv_file',
action='store_true',
default=False,
help='Use the virtualenv configuration file only'
)
self.parser.insert_option_group(0, self.cmd_opts)
def run(self, options, args):
handlers = {
"list": self.list_values,
"edit": self.open_in_editor,
"get": self.get_name,
"set": self.set_name_value,
"unset": self.unset_name
}
# Determine action
if not args or args[0] not in handlers:
logger.error("Need an action ({}) to perform.".format(
", ".join(sorted(handlers)))
)
return ERROR
action = args[0]
# Determine which configuration files are to be loaded
# Depends on whether the command is modifying.
try:
load_only = self._determine_file(
options, need_value=(action in ["get", "set", "unset", "edit"])
)
except PipError as e:
logger.error(e.args[0])
return ERROR
# Load a new configuration
self.configuration = Configuration(
isolated=options.isolated_mode, load_only=load_only
)
self.configuration.load()
# Error handling happens here, not in the action-handlers.
try:
handlers[action](options, args[1:])
except PipError as e:
logger.error(e.args[0])
return ERROR
return SUCCESS
def _determine_file(self, options, need_value):
file_options = {
kinds.USER: options.user_file,
kinds.GLOBAL: options.global_file,
kinds.VENV: options.venv_file
}
if sum(file_options.values()) == 0:
if not need_value:
return None
# Default to user, unless there's a virtualenv file.
elif os.path.exists(venv_config_file):
return kinds.VENV
else:
return kinds.USER
elif sum(file_options.values()) == 1:
# There's probably a better expression for this.
return [key for key in file_options if file_options[key]][0]
raise PipError(
"Need exactly one file to operate upon "
"(--user, --venv, --global) to perform."
)
def list_values(self, options, args):
self._get_n_args(args, "list", n=0)
for key, value in sorted(self.configuration.items()):
logger.info("%s=%r", key, value)
def get_name(self, options, args):
key = self._get_n_args(args, "get [name]", n=1)
value = self.configuration.get_value(key)
logger.info("%s", value)
def set_name_value(self, options, args):
key, value = self._get_n_args(args, "set [name] [value]", n=2)
self.configuration.set_value(key, value)
self._save_configuration()
def unset_name(self, options, args):
key = self._get_n_args(args, "unset [name]", n=1)
self.configuration.unset_value(key)
self._save_configuration()
def open_in_editor(self, options, args):
editor = self._determine_editor(options)
fname = self.configuration.get_file_to_edit()
if fname is None:
raise PipError("Could not determine appropriate file.")
try:
subprocess.check_call([editor, fname])
except subprocess.CalledProcessError as e:
raise PipError(
"Editor Subprocess exited with exit code {}"
.format(e.returncode)
)
def _get_n_args(self, args, example, n):
"""Helper to make sure the command got the right number of arguments
"""
if len(args) != n:
msg = (
'Got unexpected number of arguments, expected {}. '
'(example: "{} config {}")'
).format(n, get_prog(), example)
raise PipError(msg)
if n == 1:
return args[0]
else:
return args
def _save_configuration(self):
# We successfully ran a modifying command. Need to save the
# configuration.
try:
self.configuration.save()
except Exception:
logger.error(
"Unable to save configuration. Please report this as a bug.",
exc_info=1
)
raise PipError("Internal Error.")
def _determine_editor(self, options):
if options.editor is not None:
return options.editor
elif "VISUAL" in os.environ:
return os.environ["VISUAL"]
elif "EDITOR" in os.environ:
return os.environ["EDITOR"]
else:
raise PipError("Could not determine editor to use.")
@@ -3,15 +3,16 @@ from __future__ import absolute_import
import logging
import os
from pip9.exceptions import CommandError
from pip9.index import FormatControl
from pip9.req import RequirementSet
from pip9.basecommand import RequirementCommand
from pip9 import cmdoptions
from pip9.utils import ensure_dir, normalize_path
from pip9.utils.build import BuildDirectory
from pip9.utils.filesystem import check_path_owner
from notpip._internal import cmdoptions
from notpip._internal.basecommand import RequirementCommand
from notpip._internal.exceptions import CommandError
from notpip._internal.index import FormatControl
from notpip._internal.operations.prepare import RequirementPreparer
from notpip._internal.req import RequirementSet
from notpip._internal.resolve import Resolver
from notpip._internal.utils.filesystem import check_path_owner
from notpip._internal.utils.misc import ensure_dir, normalize_path
from notpip._internal.utils.temp_dir import TempDirectory
logger = logging.getLogger(__name__)
@@ -33,8 +34,8 @@ class DownloadCommand(RequirementCommand):
usage = """
%prog [options] <requirement specifier> [package-index-options] ...
%prog [options] -r <requirements file> [package-index-options] ...
%prog [options] [-e] <vcs project url> ...
%prog [options] [-e] <local project path> ...
%prog [options] <vcs project url> ...
%prog [options] <local project path> ...
%prog [options] <archive url/path> ..."""
summary = 'Download packages.'
@@ -45,7 +46,6 @@ class DownloadCommand(RequirementCommand):
cmd_opts = self.cmd_opts
cmd_opts.add_option(cmdoptions.constraints())
cmd_opts.add_option(cmdoptions.editable())
cmd_opts.add_option(cmdoptions.requirements())
cmd_opts.add_option(cmdoptions.build_dir())
cmd_opts.add_option(cmdoptions.no_deps())
@@ -56,6 +56,8 @@ class DownloadCommand(RequirementCommand):
cmd_opts.add_option(cmdoptions.pre())
cmd_opts.add_option(cmdoptions.no_clean())
cmd_opts.add_option(cmdoptions.require_hashes())
cmd_opts.add_option(cmdoptions.progress_bar())
cmd_opts.add_option(cmdoptions.no_build_isolation())
cmd_opts.add_option(
'-d', '--dest', '--destination-dir', '--destination-directory',
@@ -113,7 +115,7 @@ class DownloadCommand(RequirementCommand):
)
index_opts = cmdoptions.make_option_group(
cmdoptions.non_deprecated_index_group,
cmdoptions.index_group,
self.parser,
)
@@ -122,6 +124,9 @@ class DownloadCommand(RequirementCommand):
def run(self, options, args):
options.ignore_installed = True
# editable doesn't really make sense for `pip download`, but the bowels
# of the RequirementSet code require that property.
options.editables = []
if options.python_version:
python_versions = [options.python_version]
@@ -134,13 +139,18 @@ class DownloadCommand(RequirementCommand):
options.abi,
options.implementation,
])
binary_only = FormatControl(set(), set([':all:']))
if dist_restriction_set and options.format_control != binary_only:
binary_only = FormatControl(set(), {':all:'})
no_sdist_dependencies = (
options.format_control != binary_only and
not options.ignore_dependencies
)
if dist_restriction_set and no_sdist_dependencies:
raise CommandError(
"--only-binary=:all: must be set and --no-binary must not "
"be set (or must be set to :none:) when restricting platform "
"and interpreter constraints using --python-version, "
"--platform, --abi, or --implementation."
"When restricting platform and interpreter constraints using "
"--python-version, --platform, --abi, or --implementation, "
"either --no-deps must be set, or --only-binary=:all: must be "
"set and --no-binary must not be set (or must be set to "
":none:)."
)
options.src_dir = os.path.abspath(options.src_dir)
@@ -169,18 +179,12 @@ class DownloadCommand(RequirementCommand):
)
options.cache_dir = None
with BuildDirectory(options.build_dir,
delete=build_delete) as build_dir:
with TempDirectory(
options.build_dir, delete=build_delete, kind="download"
) as directory:
requirement_set = RequirementSet(
build_dir=build_dir,
src_dir=options.src_dir,
download_dir=options.download_dir,
ignore_installed=True,
ignore_dependencies=options.ignore_dependencies,
session=session,
isolated=options.isolated_mode,
require_hashes=options.require_hashes
require_hashes=options.require_hashes,
)
self.populate_requirement_set(
requirement_set,
@@ -192,18 +196,35 @@ class DownloadCommand(RequirementCommand):
None
)
if not requirement_set.has_requirements:
return
preparer = RequirementPreparer(
build_dir=directory.path,
src_dir=options.src_dir,
download_dir=options.download_dir,
wheel_download_dir=None,
progress_bar=options.progress_bar,
build_isolation=options.build_isolation,
)
requirement_set.prepare_files(finder)
resolver = Resolver(
preparer=preparer,
finder=finder,
session=session,
wheel_cache=None,
use_user_site=False,
upgrade_strategy="to-satisfy-only",
force_reinstall=False,
ignore_dependencies=options.ignore_dependencies,
ignore_requires_python=False,
ignore_installed=True,
isolated=options.isolated_mode,
)
resolver.resolve(requirement_set)
downloaded = ' '.join([
req.name for req in requirement_set.successfully_downloaded
])
if downloaded:
logger.info(
'Successfully downloaded %s', downloaded
)
logger.info('Successfully downloaded %s', downloaded)
# Clean up
if not options.no_clean:
@@ -2,14 +2,13 @@ from __future__ import absolute_import
import sys
import pip9
from pip9.compat import stdlib_pkgs
from pip9.basecommand import Command
from pip9.operations.freeze import freeze
from pip9.wheel import WheelCache
from notpip._internal import index
from notpip._internal.basecommand import Command
from notpip._internal.cache import WheelCache
from notpip._internal.compat import stdlib_pkgs
from notpip._internal.operations.freeze import freeze
DEV_PKGS = ('pip', 'setuptools', 'distribute', 'wheel')
DEV_PKGS = {'pip', 'setuptools', 'distribute', 'wheel'}
class FreezeCommand(Command):
@@ -63,11 +62,16 @@ class FreezeCommand(Command):
action='store_true',
help='Do not skip these packages in the output:'
' %s' % ', '.join(DEV_PKGS))
self.cmd_opts.add_option(
'--exclude-editable',
dest='exclude_editable',
action='store_true',
help='Exclude editable package from output.')
self.parser.insert_option_group(0, self.cmd_opts)
def run(self, options, args):
format_control = pip9.index.FormatControl(set(), set())
format_control = index.FormatControl(set(), set())
wheel_cache = WheelCache(options.cache_dir, format_control)
skip = set(stdlib_pkgs)
if not options.freeze_all:
@@ -81,7 +85,12 @@ class FreezeCommand(Command):
skip_regex=options.skip_requirements_regex,
isolated=options.isolated_mode,
wheel_cache=wheel_cache,
skip=skip)
skip=skip,
exclude_editable=options.exclude_editable,
)
for line in freeze(**freeze_kwargs):
sys.stdout.write(line + '\n')
try:
for line in freeze(**freeze_kwargs):
sys.stdout.write(line + '\n')
finally:
wheel_cache.cleanup()
@@ -4,11 +4,10 @@ import hashlib
import logging
import sys
from pip9.basecommand import Command
from pip9.status_codes import ERROR
from pip9.utils import read_chunks
from pip9.utils.hashes import FAVORITE_HASH, STRONG_HASHES
from notpip._internal.basecommand import Command
from notpip._internal.status_codes import ERROR
from notpip._internal.utils.hashes import FAVORITE_HASH, STRONG_HASHES
from notpip._internal.utils.misc import read_chunks
logger = logging.getLogger(__name__)
@@ -24,6 +23,7 @@ class HashCommand(Command):
name = 'hash'
usage = '%prog [options] <file> ...'
summary = 'Compute hashes of package archives.'
ignore_require_venv = True
def __init__(self, *args, **kw):
super(HashCommand, self).__init__(*args, **kw)
@@ -1,7 +1,7 @@
from __future__ import absolute_import
from pip9.basecommand import Command, SUCCESS
from pip9.exceptions import CommandError
from notpip._internal.basecommand import SUCCESS, Command
from notpip._internal.exceptions import CommandError
class HelpCommand(Command):
@@ -10,12 +10,13 @@ class HelpCommand(Command):
usage = """
%prog <command>"""
summary = 'Show help for commands.'
ignore_require_venv = True
def run(self, options, args):
from pip9.commands import commands_dict, get_similar_commands
from notpip._internal.commands import commands_dict, get_similar_commands
try:
# 'pip help' with no args is handled by pip9.__init__.parseopt()
# 'pip help' with no args is handled by pip.__init__.parseopt()
cmd_name = args[0] # the command we need help for
except IndexError:
return SUCCESS
@@ -0,0 +1,502 @@
from __future__ import absolute_import
import errno
import logging
import operator
import os
import shutil
from optparse import SUPPRESS_HELP
from notpip._internal import cmdoptions
from notpip._internal.basecommand import RequirementCommand
from notpip._internal.cache import WheelCache
from notpip._internal.exceptions import (
CommandError, InstallationError, PreviousBuildDirError,
)
from notpip._internal.locations import distutils_scheme, virtualenv_no_global
from notpip._internal.operations.check import check_install_conflicts
from notpip._internal.operations.prepare import RequirementPreparer
from notpip._internal.req import RequirementSet, install_given_reqs
from notpip._internal.resolve import Resolver
from notpip._internal.status_codes import ERROR
from notpip._internal.utils.filesystem import check_path_owner
from notpip._internal.utils.misc import ensure_dir, get_installed_version
from notpip._internal.utils.temp_dir import TempDirectory
from notpip._internal.wheel import WheelBuilder
try:
import wheel
except ImportError:
wheel = None
logger = logging.getLogger(__name__)
class InstallCommand(RequirementCommand):
"""
Install packages from:
- PyPI (and other indexes) using requirement specifiers.
- VCS project urls.
- Local project directories.
- Local or remote source archives.
pip also supports installing from "requirements files", which provide
an easy way to specify a whole environment to be installed.
"""
name = 'install'
usage = """
%prog [options] <requirement specifier> [package-index-options] ...
%prog [options] -r <requirements file> [package-index-options] ...
%prog [options] [-e] <vcs project url> ...
%prog [options] [-e] <local project path> ...
%prog [options] <archive url/path> ..."""
summary = 'Install packages.'
def __init__(self, *args, **kw):
super(InstallCommand, self).__init__(*args, **kw)
cmd_opts = self.cmd_opts
cmd_opts.add_option(cmdoptions.requirements())
cmd_opts.add_option(cmdoptions.constraints())
cmd_opts.add_option(cmdoptions.no_deps())
cmd_opts.add_option(cmdoptions.pre())
cmd_opts.add_option(cmdoptions.editable())
cmd_opts.add_option(
'-t', '--target',
dest='target_dir',
metavar='dir',
default=None,
help='Install packages into <dir>. '
'By default this will not replace existing files/folders in '
'<dir>. Use --upgrade to replace existing packages in <dir> '
'with new versions.'
)
cmd_opts.add_option(
'--user',
dest='use_user_site',
action='store_true',
help="Install to the Python user install directory for your "
"platform. Typically ~/.local/, or %APPDATA%\\Python on "
"Windows. (See the Python documentation for site.USER_BASE "
"for full details.)")
cmd_opts.add_option(
'--no-user',
dest='use_user_site',
action='store_false',
help=SUPPRESS_HELP)
cmd_opts.add_option(
'--root',
dest='root_path',
metavar='dir',
default=None,
help="Install everything relative to this alternate root "
"directory.")
cmd_opts.add_option(
'--prefix',
dest='prefix_path',
metavar='dir',
default=None,
help="Installation prefix where lib, bin and other top-level "
"folders are placed")
cmd_opts.add_option(cmdoptions.build_dir())
cmd_opts.add_option(cmdoptions.src())
cmd_opts.add_option(
'-U', '--upgrade',
dest='upgrade',
action='store_true',
help='Upgrade all specified packages to the newest available '
'version. The handling of dependencies depends on the '
'upgrade-strategy used.'
)
cmd_opts.add_option(
'--upgrade-strategy',
dest='upgrade_strategy',
default='only-if-needed',
choices=['only-if-needed', 'eager'],
help='Determines how dependency upgrading should be handled '
'[default: %default]. '
'"eager" - dependencies are upgraded regardless of '
'whether the currently installed version satisfies the '
'requirements of the upgraded package(s). '
'"only-if-needed" - are upgraded only when they do not '
'satisfy the requirements of the upgraded package(s).'
)
cmd_opts.add_option(
'--force-reinstall',
dest='force_reinstall',
action='store_true',
help='Reinstall all packages even if they are already '
'up-to-date.')
cmd_opts.add_option(
'-I', '--ignore-installed',
dest='ignore_installed',
action='store_true',
help='Ignore the installed packages (reinstalling instead).')
cmd_opts.add_option(cmdoptions.ignore_requires_python())
cmd_opts.add_option(cmdoptions.no_build_isolation())
cmd_opts.add_option(cmdoptions.install_options())
cmd_opts.add_option(cmdoptions.global_options())
cmd_opts.add_option(
"--compile",
action="store_true",
dest="compile",
default=True,
help="Compile Python source files to bytecode",
)
cmd_opts.add_option(
"--no-compile",
action="store_false",
dest="compile",
help="Do not compile Python source files to bytecode",
)
cmd_opts.add_option(
"--no-warn-script-location",
action="store_false",
dest="warn_script_location",
default=True,
help="Do not warn when installing scripts outside PATH",
)
cmd_opts.add_option(
"--no-warn-conflicts",
action="store_false",
dest="warn_about_conflicts",
default=True,
help="Do not warn about broken dependencies",
)
cmd_opts.add_option(cmdoptions.no_binary())
cmd_opts.add_option(cmdoptions.only_binary())
cmd_opts.add_option(cmdoptions.no_clean())
cmd_opts.add_option(cmdoptions.require_hashes())
cmd_opts.add_option(cmdoptions.progress_bar())
index_opts = cmdoptions.make_option_group(
cmdoptions.index_group,
self.parser,
)
self.parser.insert_option_group(0, index_opts)
self.parser.insert_option_group(0, cmd_opts)
def run(self, options, args):
cmdoptions.check_install_build_global(options)
upgrade_strategy = "to-satisfy-only"
if options.upgrade:
upgrade_strategy = options.upgrade_strategy
if options.build_dir:
options.build_dir = os.path.abspath(options.build_dir)
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=')
target_temp_dir = TempDirectory(kind="target")
if options.target_dir:
options.ignore_installed = True
options.target_dir = os.path.abspath(options.target_dir)
if (os.path.exists(options.target_dir) and not
os.path.isdir(options.target_dir)):
raise CommandError(
"Target path exists but is not a directory, will not "
"continue."
)
# Create a target directory for using with the target option
target_temp_dir.create()
install_options.append('--home=' + target_temp_dir.path)
global_options = options.global_options or []
with self._build_session(options) as session:
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)
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 TempDirectory(
options.build_dir, delete=build_delete, kind="install"
) as directory:
requirement_set = RequirementSet(
require_hashes=options.require_hashes,
)
try:
self.populate_requirement_set(
requirement_set, args, options, finder, session,
self.name, wheel_cache
)
preparer = RequirementPreparer(
build_dir=directory.path,
src_dir=options.src_dir,
download_dir=None,
wheel_download_dir=None,
progress_bar=options.progress_bar,
build_isolation=options.build_isolation,
)
resolver = Resolver(
preparer=preparer,
finder=finder,
session=session,
wheel_cache=wheel_cache,
use_user_site=options.use_user_site,
upgrade_strategy=upgrade_strategy,
force_reinstall=options.force_reinstall,
ignore_dependencies=options.ignore_dependencies,
ignore_requires_python=options.ignore_requires_python,
ignore_installed=options.ignore_installed,
isolated=options.isolated_mode,
)
resolver.resolve(requirement_set)
# If caching is disabled or wheel is not installed don't
# try to build wheels.
if wheel and options.cache_dir:
# build wheels before install.
wb = WheelBuilder(
finder, preparer, wheel_cache,
build_options=[], global_options=[],
)
# Ignore the result: a failed wheel will be
# installed from the sdist/vcs whatever.
wb.build(
requirement_set.requirements.values(),
session=session, autobuilding=True
)
to_install = resolver.get_installation_order(
requirement_set
)
# Consistency Checking of the package set we're installing.
should_warn_about_conflicts = (
not options.ignore_dependencies and
options.warn_about_conflicts
)
if should_warn_about_conflicts:
self._warn_about_conflicts(to_install)
# Don't warn about script install locations if
# --target has been specified
warn_script_location = options.warn_script_location
if options.target_dir:
warn_script_location = False
installed = install_given_reqs(
to_install,
install_options,
global_options,
root=options.root_path,
home=target_temp_dir.path,
prefix=options.prefix_path,
pycompile=options.compile,
warn_script_location=warn_script_location,
use_user_site=options.use_user_site,
)
possible_lib_locations = get_lib_location_guesses(
user=options.use_user_site,
home=target_temp_dir.path,
root=options.root_path,
prefix=options.prefix_path,
isolated=options.isolated_mode,
)
reqs = sorted(installed, key=operator.attrgetter('name'))
items = []
for req in reqs:
item = req.name
try:
installed_version = get_installed_version(
req.name, possible_lib_locations
)
if installed_version:
item += '-' + installed_version
except Exception:
pass
items.append(item)
installed = ' '.join(items)
if installed:
logger.info('Successfully installed %s', installed)
except EnvironmentError as error:
show_traceback = (self.verbosity >= 1)
message = create_env_error_message(
error, show_traceback, options.use_user_site,
)
logger.error(message, exc_info=show_traceback)
return ERROR
except PreviousBuildDirError:
options.no_clean = True
raise
finally:
# Clean up
if not options.no_clean:
requirement_set.cleanup_files()
wheel_cache.cleanup()
if options.target_dir:
self._handle_target_dir(
options.target_dir, target_temp_dir, options.upgrade
)
return requirement_set
def _handle_target_dir(self, target_dir, target_temp_dir, upgrade):
ensure_dir(target_dir)
# Checking both purelib and platlib directories for installed
# packages to be moved to target directory
lib_dir_list = []
with target_temp_dir:
# Checking both purelib and platlib directories for installed
# packages to be moved to target directory
scheme = distutils_scheme('', home=target_temp_dir.path)
purelib_dir = scheme['purelib']
platlib_dir = scheme['platlib']
data_dir = scheme['data']
if os.path.exists(purelib_dir):
lib_dir_list.append(purelib_dir)
if os.path.exists(platlib_dir) and platlib_dir != purelib_dir:
lib_dir_list.append(platlib_dir)
if os.path.exists(data_dir):
lib_dir_list.append(data_dir)
for lib_dir in lib_dir_list:
for item in os.listdir(lib_dir):
if lib_dir == data_dir:
ddir = os.path.join(data_dir, item)
if any(s.startswith(ddir) for s in lib_dir_list[:-1]):
continue
target_item_dir = os.path.join(target_dir, item)
if os.path.exists(target_item_dir):
if not upgrade:
logger.warning(
'Target directory %s already exists. Specify '
'--upgrade to force replacement.',
target_item_dir
)
continue
if os.path.islink(target_item_dir):
logger.warning(
'Target directory %s already exists and is '
'a link. Pip will not automatically replace '
'links, please remove if replacement is '
'desired.',
target_item_dir
)
continue
if os.path.isdir(target_item_dir):
shutil.rmtree(target_item_dir)
else:
os.remove(target_item_dir)
shutil.move(
os.path.join(lib_dir, item),
target_item_dir
)
def _warn_about_conflicts(self, to_install):
package_set, _dep_info = check_install_conflicts(to_install)
missing, conflicting = _dep_info
# NOTE: There is some duplication here from notpip check
for project_name in missing:
version = package_set[project_name][0]
for dependency in missing[project_name]:
logger.critical(
"%s %s requires %s, which is not installed.",
project_name, version, dependency[1],
)
for project_name in conflicting:
version = package_set[project_name][0]
for dep_name, dep_version, req in conflicting[project_name]:
logger.critical(
"%s %s has requirement %s, but you'll have %s %s which is "
"incompatible.",
project_name, version, req, dep_name, dep_version,
)
def get_lib_location_guesses(*args, **kwargs):
scheme = distutils_scheme('', *args, **kwargs)
return [scheme['purelib'], scheme['platlib']]
def create_env_error_message(error, show_traceback, using_user_site):
"""Format an error message for an EnvironmentError
It may occur anytime during the execution of the install command.
"""
parts = []
# Mention the error if we are not going to show a traceback
parts.append("Could not install packages due to an EnvironmentError")
if not show_traceback:
parts.append(": ")
parts.append(str(error))
else:
parts.append(".")
# Spilt the error indication from a helper message (if any)
parts[-1] += "\n"
# Suggest useful actions to the user:
# (1) using user site-packages or (2) verifying the permissions
if error.errno == errno.EACCES:
user_option_part = "Consider using the `--user` option"
permissions_part = "Check the permissions"
if not using_user_site:
parts.extend([
user_option_part, " or ",
permissions_part.lower(),
])
else:
parts.append(permissions_part)
parts.append(".\n")
return "".join(parts).strip() + "\n"
@@ -3,20 +3,19 @@ from __future__ import absolute_import
import json
import logging
import warnings
try:
from itertools import zip_longest
except ImportError:
from itertools import izip_longest as zip_longest
from pip9._vendor import six
from notpip._vendor import six
from notpip._vendor.six.moves import zip_longest
from pip9.basecommand import Command
from pip9.exceptions import CommandError
from pip9.index import PackageFinder
from pip9.utils import (
get_installed_distributions, dist_is_editable)
from pip9.utils.deprecation import RemovedInPip10Warning
from pip9.cmdoptions import make_option_group, index_group
from notpip._internal.basecommand import Command
from notpip._internal.cmdoptions import index_group, make_option_group
from notpip._internal.exceptions import CommandError
from notpip._internal.index import PackageFinder
from notpip._internal.utils.deprecation import RemovedInPip11Warning
from notpip._internal.utils.misc import (
dist_is_editable, get_installed_distributions,
)
from notpip._internal.utils.packaging import get_installer
logger = logging.getLogger(__name__)
@@ -78,9 +77,10 @@ class ListCommand(Command):
'--format',
action='store',
dest='list_format',
default="columns",
choices=('legacy', 'columns', 'freeze', 'json'),
help="Select the output format among: legacy (default), columns, "
"freeze or json.",
help="Select the output format among: columns (default), freeze, "
"json, or legacy.",
)
cmd_opts.add_option(
@@ -91,6 +91,19 @@ class ListCommand(Command):
"installed packages.",
)
cmd_opts.add_option(
'--exclude-editable',
action='store_false',
dest='include_editable',
help='Exclude editable package from output.',
)
cmd_opts.add_option(
'--include-editable',
action='store_true',
dest='include_editable',
help='Include editable package from output.',
default=True,
)
index_opts = make_option_group(index_group, self.parser)
self.parser.insert_option_group(0, index_opts)
@@ -110,37 +123,11 @@ class ListCommand(Command):
)
def run(self, options, args):
if options.allow_external:
if options.list_format == "legacy":
warnings.warn(
"--allow-external has been deprecated and will be removed in "
"the future. Due to changes in the repository protocol, it no "
"longer has any effect.",
RemovedInPip10Warning,
)
if options.allow_all_external:
warnings.warn(
"--allow-all-external has been deprecated and will be removed "
"in the future. Due to changes in the repository protocol, it "
"no longer has any effect.",
RemovedInPip10Warning,
)
if options.allow_unverified:
warnings.warn(
"--allow-unverified has been deprecated and will be removed "
"in the future. Due to changes in the repository protocol, it "
"no longer has any effect.",
RemovedInPip10Warning,
)
if options.list_format is None:
warnings.warn(
"The default format will switch to columns in the future. "
"You can use --format=(legacy|columns) (or define a "
"format=(legacy|columns) in your pip9.conf under the [list] "
"section) to disable this warning.",
RemovedInPip10Warning,
"The legacy format has been deprecated and will be removed "
"in the future.",
RemovedInPip11Warning,
)
if options.outdated and options.uptodate:
@@ -151,6 +138,7 @@ class ListCommand(Command):
local_only=options.local,
user_only=options.user,
editables_only=options.editable,
include_editables=options.include_editable,
)
if options.outdated:
@@ -179,7 +167,7 @@ class ListCommand(Command):
dep_keys = set()
for dist in packages:
dep_keys.update(requirement.key for requirement in dist.requires())
return set(pkg for pkg in packages if pkg.key not in dep_keys)
return {pkg for pkg in packages if pkg.key not in dep_keys}
def iter_packages_latest_infos(self, packages, options):
index_urls = [options.index_url] + options.extra_index_urls
@@ -220,8 +208,15 @@ class ListCommand(Command):
dist.latest_filetype = typ
yield dist
def output_legacy(self, dist):
if dist_is_editable(dist):
def output_legacy(self, dist, options):
if options.verbose >= 1:
return '%s (%s, %s, %s)' % (
dist.project_name,
dist.version,
dist.location,
get_installer(dist),
)
elif dist_is_editable(dist):
return '%s (%s, %s)' % (
dist.project_name,
dist.version,
@@ -230,9 +225,9 @@ class ListCommand(Command):
else:
return '%s (%s)' % (dist.project_name, dist.version)
def output_legacy_latest(self, dist):
def output_legacy_latest(self, dist, options):
return '%s - Latest: %s [%s]' % (
self.output_legacy(dist),
self.output_legacy(dist, options),
dist.latest_version,
dist.latest_filetype,
)
@@ -247,15 +242,19 @@ class ListCommand(Command):
self.output_package_listing_columns(data, header)
elif options.list_format == 'freeze':
for dist in packages:
logger.info("%s==%s", dist.project_name, dist.version)
if options.verbose >= 1:
logger.info("%s==%s (%s)", dist.project_name,
dist.version, dist.location)
else:
logger.info("%s==%s", dist.project_name, dist.version)
elif options.list_format == 'json':
logger.info(format_for_json(packages, options))
else: # legacy
elif options.list_format == "legacy":
for dist in packages:
if options.outdated:
logger.info(self.output_legacy_latest(dist))
logger.info(self.output_legacy_latest(dist, options))
else:
logger.info(self.output_legacy(dist))
logger.info(self.output_legacy(dist, options))
def output_package_listing_columns(self, data, header):
# insert the header first: we need to know the size of column names
@@ -303,8 +302,10 @@ def format_for_columns(pkgs, options):
header = ["Package", "Version"]
data = []
if any(dist_is_editable(x) for x in pkgs):
if options.verbose >= 1 or any(dist_is_editable(x) for x in pkgs):
header.append("Location")
if options.verbose >= 1:
header.append("Installer")
for proj in pkgs:
# if we're working on the 'outdated' list, separate out the
@@ -315,8 +316,10 @@ def format_for_columns(pkgs, options):
row.append(proj.latest_version)
row.append(proj.latest_filetype)
if dist_is_editable(proj):
if options.verbose >= 1 or dist_is_editable(proj):
row.append(proj.location)
if options.verbose >= 1:
row.append(get_installer(proj))
data.append(row)
@@ -330,6 +333,9 @@ def format_for_json(packages, options):
'name': dist.project_name,
'version': six.text_type(dist.version),
}
if options.verbose >= 1:
info['location'] = dist.location
info['installer'] = get_installer(dist)
if options.outdated:
info['latest_version'] = six.text_type(dist.latest_version)
info['latest_filetype'] = dist.latest_filetype
@@ -3,19 +3,21 @@ from __future__ import absolute_import
import logging
import sys
import textwrap
from collections import OrderedDict
from pip9.basecommand import Command, SUCCESS
from pip9.compat import OrderedDict
from pip9.download import pip9XmlrpcTransport
from pip9.models import PyPI
from pip9.utils import get_terminal_size
from pip9.utils.logging import indent_log
from pip9.exceptions import CommandError
from pip9.status_codes import NO_MATCHES_FOUND
from pip9._vendor.packaging.version import parse as parse_version
from pip9._vendor import pkg_resources
from pip9._vendor.six.moves import xmlrpc_client
from notpip._vendor import pkg_resources
from notpip._vendor.packaging.version import parse as parse_version
# NOTE: XMLRPC Client is not annotated in typeshed as on 2017-07-17, which is
# why we ignore the type on this import
from notpip._vendor.six.moves import xmlrpc_client # type: ignore
from notpip._internal.basecommand import SUCCESS, Command
from notpip._internal.compat import get_terminal_size
from notpip._internal.download import PipXmlrpcTransport
from notpip._internal.exceptions import CommandError
from notpip._internal.models import PyPI
from notpip._internal.status_codes import NO_MATCHES_FOUND
from notpip._internal.utils.logging import indent_log
logger = logging.getLogger(__name__)
@@ -26,6 +28,7 @@ class SearchCommand(Command):
usage = """
%prog [options] <query>"""
summary = 'Search PyPI for packages.'
ignore_require_venv = True
def __init__(self, *args, **kw):
super(SearchCommand, self).__init__(*args, **kw)
@@ -96,7 +99,7 @@ def print_results(hits, name_column_width=None, terminal_width=None):
return
if name_column_width is None:
name_column_width = max([
len(hit['name']) + len(hit.get('versions', ['-'])[-1])
len(hit['name']) + len(highest_version(hit.get('versions', ['-'])))
for hit in hits
]) + 4
@@ -104,7 +107,7 @@ def print_results(hits, name_column_width=None, terminal_width=None):
for hit in hits:
name = hit['name']
summary = hit['summary'] or ''
version = hit.get('versions', ['-'])[-1]
latest = highest_version(hit.get('versions', ['-']))
if terminal_width is not None:
target_width = terminal_width - name_column_width - 5
if target_width > 10:
@@ -113,13 +116,12 @@ def print_results(hits, name_column_width=None, terminal_width=None):
summary = ('\n' + ' ' * (name_column_width + 3)).join(summary)
line = '%-*s - %s' % (name_column_width,
'%s (%s)' % (name, version), summary)
'%s (%s)' % (name, latest), summary)
try:
logger.info(line)
if name in installed_packages:
dist = pkg_resources.get_distribution(name)
with indent_log():
latest = highest_version(hit['versions'])
if dist.version == latest:
logger.info('INSTALLED: %s (latest)', dist.version)
else:
@@ -1,14 +1,14 @@
from __future__ import absolute_import
from email.parser import FeedParser
import logging
import os
from email.parser import FeedParser # type: ignore
from pip9.basecommand import Command
from pip9.status_codes import SUCCESS, ERROR
from pip9._vendor import pkg_resources
from pip9._vendor.packaging.utils import canonicalize_name
from notpip._vendor import pkg_resources
from notpip._vendor.packaging.utils import canonicalize_name
from notpip._internal.basecommand import Command
from notpip._internal.status_codes import ERROR, SUCCESS
logger = logging.getLogger(__name__)
@@ -19,6 +19,7 @@ class ShowCommand(Command):
usage = """
%prog [options] <package> ..."""
summary = 'Show information about installed packages.'
ignore_require_venv = True
def __init__(self, *args, **kw):
super(ShowCommand, self).__init__(*args, **kw)
@@ -126,7 +127,14 @@ def print_results(distributions, list_files=False, verbose=False):
results_printed = True
if i > 0:
logger.info("---")
logger.info("Name: %s", dist.get('name', ''))
name = dist.get('name', '')
required_by = [
pkg.project_name for pkg in pkg_resources.working_set
if name in [required.name for required in pkg.requires()]
]
logger.info("Name: %s", name)
logger.info("Version: %s", dist.get('version', ''))
logger.info("Summary: %s", dist.get('summary', ''))
logger.info("Home-page: %s", dist.get('home-page', ''))
@@ -135,6 +143,8 @@ def print_results(distributions, list_files=False, verbose=False):
logger.info("License: %s", dist.get('license', ''))
logger.info("Location: %s", dist.get('location', ''))
logger.info("Requires: %s", ', '.join(dist.get('requires', [])))
logger.info("Required-by: %s", ', '.join(required_by))
if verbose:
logger.info("Metadata-Version: %s",
dist.get('metadata-version', ''))
@@ -1,10 +1,10 @@
from __future__ import absolute_import
import pip9
from pip9.wheel import WheelCache
from pip9.req import InstallRequirement, RequirementSet, parse_requirements
from pip9.basecommand import Command
from pip9.exceptions import InstallationError
from notpip._vendor.packaging.utils import canonicalize_name
from notpip._internal.basecommand import Command
from notpip._internal.exceptions import InstallationError
from notpip._internal.req import InstallRequirement, parse_requirements
class UninstallCommand(Command):
@@ -44,33 +44,28 @@ class UninstallCommand(Command):
def run(self, options, args):
with self._build_session(options) as session:
format_control = pip9.index.FormatControl(set(), set())
wheel_cache = WheelCache(options.cache_dir, format_control)
requirement_set = RequirementSet(
build_dir=None,
src_dir=None,
download_dir=None,
isolated=options.isolated_mode,
session=session,
wheel_cache=wheel_cache,
)
reqs_to_uninstall = {}
for name in args:
requirement_set.add_requirement(
InstallRequirement.from_line(
name, isolated=options.isolated_mode,
wheel_cache=wheel_cache
)
req = InstallRequirement.from_line(
name, isolated=options.isolated_mode,
)
if req.name:
reqs_to_uninstall[canonicalize_name(req.name)] = req
for filename in options.requirements:
for req in parse_requirements(
filename,
options=options,
session=session,
wheel_cache=wheel_cache):
requirement_set.add_requirement(req)
if not requirement_set.has_requirements:
session=session):
if req.name:
reqs_to_uninstall[canonicalize_name(req.name)] = req
if not reqs_to_uninstall:
raise InstallationError(
'You must give at least one requirement to %(name)s (see '
'"pip help %(name)s")' % dict(name=self.name)
)
requirement_set.uninstall(auto_confirm=options.yes)
for req in reqs_to_uninstall.values():
uninstall_pathset = req.uninstall(
auto_confirm=options.yes, verbose=self.verbosity > 0,
)
if uninstall_pathset:
uninstall_pathset.commit()
@@ -3,17 +3,16 @@ from __future__ import absolute_import
import logging
import os
import warnings
from pip9.basecommand import RequirementCommand
from pip9.exceptions import CommandError, PreviousBuildDirError
from pip9.req import RequirementSet
from pip9.utils import import_or_raise
from pip9.utils.build import BuildDirectory
from pip9.utils.deprecation import RemovedInPip10Warning
from pip9.wheel import WheelCache, WheelBuilder
from pip9 import cmdoptions
from notpip._internal import cmdoptions
from notpip._internal.basecommand import RequirementCommand
from notpip._internal.cache import WheelCache
from notpip._internal.exceptions import CommandError, PreviousBuildDirError
from notpip._internal.operations.prepare import RequirementPreparer
from notpip._internal.req import RequirementSet
from notpip._internal.resolve import Resolver
from notpip._internal.utils.temp_dir import TempDirectory
from notpip._internal.wheel import WheelBuilder
logger = logging.getLogger(__name__)
@@ -56,8 +55,6 @@ class WheelCommand(RequirementCommand):
help=("Build wheels into <dir>, where the default is the "
"current working directory."),
)
cmd_opts.add_option(cmdoptions.use_wheel())
cmd_opts.add_option(cmdoptions.no_use_wheel())
cmd_opts.add_option(cmdoptions.no_binary())
cmd_opts.add_option(cmdoptions.only_binary())
cmd_opts.add_option(
@@ -65,7 +62,9 @@ class WheelCommand(RequirementCommand):
dest='build_options',
metavar='options',
action='append',
help="Extra arguments to be supplied to 'setup.py bdist_wheel'.")
help="Extra arguments to be supplied to 'setup.py bdist_wheel'.",
)
cmd_opts.add_option(cmdoptions.no_build_isolation())
cmd_opts.add_option(cmdoptions.constraints())
cmd_opts.add_option(cmdoptions.editable())
cmd_opts.add_option(cmdoptions.requirements())
@@ -73,6 +72,7 @@ class WheelCommand(RequirementCommand):
cmd_opts.add_option(cmdoptions.ignore_requires_python())
cmd_opts.add_option(cmdoptions.no_deps())
cmd_opts.add_option(cmdoptions.build_dir())
cmd_opts.add_option(cmdoptions.progress_bar())
cmd_opts.add_option(
'--global-option',
@@ -101,55 +101,9 @@ class WheelCommand(RequirementCommand):
self.parser.insert_option_group(0, index_opts)
self.parser.insert_option_group(0, cmd_opts)
def check_required_packages(self):
import_or_raise(
'wheel.bdist_wheel',
CommandError,
"'pip wheel' requires the 'wheel' package. To fix this, run: "
"pip install wheel"
)
pkg_resources = import_or_raise(
'pkg_resources',
CommandError,
"'pip wheel' requires setuptools >= 0.8 for dist-info support."
" To fix this, run: pip install --upgrade setuptools"
)
if not hasattr(pkg_resources, 'DistInfoDistribution'):
raise CommandError(
"'pip wheel' requires setuptools >= 0.8 for dist-info "
"support. To fix this, run: pip install --upgrade "
"setuptools"
)
def run(self, options, args):
self.check_required_packages()
cmdoptions.resolve_wheel_no_use_binary(options)
cmdoptions.check_install_build_global(options)
if options.allow_external:
warnings.warn(
"--allow-external has been deprecated and will be removed in "
"the future. Due to changes in the repository protocol, it no "
"longer has any effect.",
RemovedInPip10Warning,
)
if options.allow_all_external:
warnings.warn(
"--allow-all-external has been deprecated and will be removed "
"in the future. Due to changes in the repository protocol, it "
"no longer has any effect.",
RemovedInPip10Warning,
)
if options.allow_unverified:
warnings.warn(
"--allow-unverified has been deprecated and will be removed "
"in the future. Due to changes in the repository protocol, it "
"no longer has any effect.",
RemovedInPip10Warning,
)
index_urls = [options.index_url] + options.extra_index_urls
if options.no_index:
logger.debug('Ignoring indexes: %s', ','.join(index_urls))
@@ -164,39 +118,55 @@ class WheelCommand(RequirementCommand):
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 BuildDirectory(options.build_dir,
delete=build_delete) as build_dir:
with TempDirectory(
options.build_dir, delete=build_delete, kind="wheel"
) as directory:
requirement_set = RequirementSet(
build_dir=build_dir,
src_dir=options.src_dir,
download_dir=None,
ignore_dependencies=options.ignore_dependencies,
ignore_installed=True,
ignore_requires_python=options.ignore_requires_python,
isolated=options.isolated_mode,
session=session,
wheel_cache=wheel_cache,
wheel_download_dir=options.wheel_dir,
require_hashes=options.require_hashes
require_hashes=options.require_hashes,
)
self.populate_requirement_set(
requirement_set, args, options, finder, session, self.name,
wheel_cache
)
if not requirement_set.has_requirements:
return
try:
self.populate_requirement_set(
requirement_set, args, options, finder, session,
self.name, wheel_cache
)
preparer = RequirementPreparer(
build_dir=directory.path,
src_dir=options.src_dir,
download_dir=None,
wheel_download_dir=options.wheel_dir,
progress_bar=options.progress_bar,
build_isolation=options.build_isolation,
)
resolver = Resolver(
preparer=preparer,
finder=finder,
session=session,
wheel_cache=wheel_cache,
use_user_site=False,
upgrade_strategy="to-satisfy-only",
force_reinstall=False,
ignore_dependencies=options.ignore_dependencies,
ignore_requires_python=options.ignore_requires_python,
ignore_installed=True,
isolated=options.isolated_mode,
)
resolver.resolve(requirement_set)
# build wheels
wb = WheelBuilder(
requirement_set,
finder,
finder, preparer, wheel_cache,
build_options=options.build_options or [],
global_options=options.global_options or [],
no_clean=options.no_clean,
)
if not wb.build():
wheels_built_successfully = wb.build(
requirement_set.requirements.values(), session=session,
)
if not wheels_built_successfully:
raise CommandError(
"Failed to build one or more wheels"
)
@@ -206,3 +176,4 @@ class WheelCommand(RequirementCommand):
finally:
if not options.no_clean:
requirement_set.cleanup_files()
wheel_cache.cleanup()
+235
View File
@@ -0,0 +1,235 @@
"""Stuff that differs in different Python versions and platform
distributions."""
from __future__ import absolute_import, division
import codecs
import locale
import logging
import os
import shutil
import sys
from notpip._vendor.six import text_type
try:
import ipaddress
except ImportError:
try:
from notpip._vendor import ipaddress # type: ignore
except ImportError:
import ipaddr as ipaddress # type: ignore
ipaddress.ip_address = ipaddress.IPAddress
ipaddress.ip_network = ipaddress.IPNetwork
__all__ = [
"ipaddress", "uses_pycache", "console_to_str", "native_str",
"get_path_uid", "stdlib_pkgs", "WINDOWS", "samefile", "get_terminal_size",
]
logger = logging.getLogger(__name__)
if sys.version_info >= (3, 4):
uses_pycache = True
from importlib.util import cache_from_source
else:
import imp
try:
cache_from_source = imp.cache_from_source # type: ignore
except AttributeError:
# does not use __pycache__
cache_from_source = None
uses_pycache = cache_from_source is not None
if sys.version_info >= (3, 5):
backslashreplace_decode = "backslashreplace"
else:
# In version 3.4 and older, backslashreplace exists
# but does not support use for decoding.
# We implement our own replace handler for this
# situation, so that we can consistently use
# backslash replacement for all versions.
def backslashreplace_decode_fn(err):
raw_bytes = (err.object[i] for i in range(err.start, err.end))
if sys.version_info[0] == 2:
# Python 2 gave us characters - convert to numeric bytes
raw_bytes = (ord(b) for b in raw_bytes)
return u"".join(u"\\x%x" % c for c in raw_bytes), err.end
codecs.register_error(
"backslashreplace_decode",
backslashreplace_decode_fn,
)
backslashreplace_decode = "backslashreplace_decode"
def console_to_str(data):
"""Return a string, safe for output, of subprocess output.
We assume the data is in the locale preferred encoding.
If it won't decode properly, we warn the user but decode as
best we can.
We also ensure that the output can be safely written to
standard output without encoding errors.
"""
# First, get the encoding we assume. This is the preferred
# encoding for the locale, unless that is not found, or
# it is ASCII, in which case assume UTF-8
encoding = locale.getpreferredencoding()
if (not encoding) or codecs.lookup(encoding).name == "ascii":
encoding = "utf-8"
# Now try to decode the data - if we fail, warn the user and
# decode with replacement.
try:
s = data.decode(encoding)
except UnicodeDecodeError:
logger.warning(
"Subprocess output does not appear to be encoded as %s",
encoding,
)
s = data.decode(encoding, errors=backslashreplace_decode)
# Make sure we can print the output, by encoding it to the output
# encoding with replacement of unencodable characters, and then
# decoding again.
# We use stderr's encoding because it's less likely to be
# redirected and if we don't find an encoding we skip this
# step (on the assumption that output is wrapped by something
# that won't fail).
# The double getattr is to deal with the possibility that we're
# being called in a situation where sys.__stderr__ doesn't exist,
# or doesn't have an encoding attribute. Neither of these cases
# should occur in normal pip use, but there's no harm in checking
# in case people use pip in (unsupported) unusual situations.
output_encoding = getattr(getattr(sys, "__stderr__", None),
"encoding", None)
if output_encoding:
s = s.encode(output_encoding, errors="backslashreplace")
s = s.decode(output_encoding)
return s
if sys.version_info >= (3,):
def native_str(s, replace=False):
if isinstance(s, bytes):
return s.decode('utf-8', 'replace' if replace else 'strict')
return s
else:
def native_str(s, replace=False):
# Replace is ignored -- unicode to UTF-8 can't fail
if isinstance(s, text_type):
return s.encode('utf-8')
return s
def get_path_uid(path):
"""
Return path's uid.
Does not follow symlinks:
https://github.com/pypa/pip/pull/935#discussion_r5307003
Placed this function in compat due to differences on AIX and
Jython, that should eventually go away.
:raises OSError: When path is a symlink or can't be read.
"""
if hasattr(os, 'O_NOFOLLOW'):
fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW)
file_uid = os.fstat(fd).st_uid
os.close(fd)
else: # AIX and Jython
# WARNING: time of check vulnerability, but best we can do w/o NOFOLLOW
if not os.path.islink(path):
# older versions of Jython don't have `os.fstat`
file_uid = os.stat(path).st_uid
else:
# raise OSError for parity with os.O_NOFOLLOW above
raise OSError(
"%s is a symlink; Will not return uid for symlinks" % path
)
return file_uid
def expanduser(path):
"""
Expand ~ and ~user constructions.
Includes a workaround for http://bugs.python.org/issue14768
"""
expanded = os.path.expanduser(path)
if path.startswith('~/') and expanded.startswith('//'):
expanded = expanded[1:]
return expanded
# packages in the stdlib that may have installation metadata, but should not be
# considered 'installed'. this theoretically could be determined based on
# dist.location (py27:`sysconfig.get_paths()['stdlib']`,
# py26:sysconfig.get_config_vars('LIBDEST')), but fear platform variation may
# make this ineffective, so hard-coding
stdlib_pkgs = {"python", "wsgiref", "argparse"}
# windows detection, covers cpython and ironpython
WINDOWS = (sys.platform.startswith("win") or
(sys.platform == 'cli' and os.name == 'nt'))
def samefile(file1, file2):
"""Provide an alternative for os.path.samefile on Windows/Python2"""
if hasattr(os.path, 'samefile'):
return os.path.samefile(file1, file2)
else:
path1 = os.path.normcase(os.path.abspath(file1))
path2 = os.path.normcase(os.path.abspath(file2))
return path1 == path2
if hasattr(shutil, 'get_terminal_size'):
def get_terminal_size():
"""
Returns a tuple (x, y) representing the width(x) and the height(y)
in characters of the terminal window.
"""
return tuple(shutil.get_terminal_size())
else:
def get_terminal_size():
"""
Returns a tuple (x, y) representing the width(x) and the height(y)
in characters of the terminal window.
"""
def ioctl_GWINSZ(fd):
try:
import fcntl
import termios
import struct
cr = struct.unpack_from(
'hh',
fcntl.ioctl(fd, termios.TIOCGWINSZ, '12345678')
)
except:
return None
if cr == (0, 0):
return None
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:
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,378 @@
"""Configuration management setup
Some terminology:
- name
As written in config files.
- value
Value associated with a name
- key
Name combined with it's section (section.name)
- variant
A single word describing where the configuration key-value pair came from
"""
import locale
import logging
import os
from notpip._vendor import six
from notpip._vendor.six.moves import configparser
from notpip._internal.exceptions import ConfigurationError
from notpip._internal.locations import (
legacy_config_file, new_config_file, running_under_virtualenv,
site_config_files, venv_config_file,
)
from notpip._internal.utils.misc import ensure_dir, enum
from notpip._internal.utils.typing import MYPY_CHECK_RUNNING
if MYPY_CHECK_RUNNING:
from typing import Any, Dict, Iterable, List, NewType, Optional, Tuple
RawConfigParser = configparser.RawConfigParser # Shorthand
Kind = NewType("Kind", str)
logger = logging.getLogger(__name__)
# NOTE: Maybe use the optionx attribute to normalize keynames.
def _normalize_name(name):
# type: (str) -> str
"""Make a name consistent regardless of source (environment or file)
"""
name = name.lower().replace('_', '-')
if name.startswith('--'):
name = name[2:] # only prefer long opts
return name
def _disassemble_key(name):
# type: (str) -> List[str]
return name.split(".", 1)
# The kinds of configurations there are.
kinds = enum(
USER="user", # User Specific
GLOBAL="global", # System Wide
VENV="venv", # Virtual Environment Specific
ENV="env", # from PIP_CONFIG_FILE
ENV_VAR="env-var", # from Environment Variables
)
class Configuration(object):
"""Handles management of configuration.
Provides an interface to accessing and managing configuration files.
This class converts provides an API that takes "section.key-name" style
keys and stores the value associated with it as "key-name" under the
section "section".
This allows for a clean interface wherein the both the section and the
key-name are preserved in an easy to manage form in the configuration files
and the data stored is also nice.
"""
def __init__(self, isolated, load_only=None):
# type: (bool, Kind) -> None
super(Configuration, self).__init__()
_valid_load_only = [kinds.USER, kinds.GLOBAL, kinds.VENV, None]
if load_only not in _valid_load_only:
raise ConfigurationError(
"Got invalid value for load_only - should be one of {}".format(
", ".join(map(repr, _valid_load_only[:-1]))
)
)
self.isolated = isolated # type: bool
self.load_only = load_only # type: Optional[Kind]
# The order here determines the override order.
self._override_order = [
kinds.GLOBAL, kinds.USER, kinds.VENV, kinds.ENV, kinds.ENV_VAR
]
self._ignore_env_names = ["version", "help"]
# Because we keep track of where we got the data from
self._parsers = {
variant: [] for variant in self._override_order
} # type: Dict[Kind, List[Tuple[str, RawConfigParser]]]
self._config = {
variant: {} for variant in self._override_order
} # type: Dict[Kind, Dict[str, Any]]
self._modified_parsers = [] # type: List[Tuple[str, RawConfigParser]]
def load(self):
# type: () -> None
"""Loads configuration from configuration files and environment
"""
self._load_config_files()
if not self.isolated:
self._load_environment_vars()
def get_file_to_edit(self):
# type: () -> Optional[str]
"""Returns the file with highest priority in configuration
"""
assert self.load_only is not None, \
"Need to be specified a file to be editing"
try:
return self._get_parser_to_modify()[0]
except IndexError:
return None
def items(self):
# type: () -> Iterable[Tuple[str, Any]]
"""Returns key-value pairs like dict.items() representing the loaded
configuration
"""
return self._dictionary.items()
def get_value(self, key):
# type: (str) -> Any
"""Get a value from the configuration.
"""
try:
return self._dictionary[key]
except KeyError:
raise ConfigurationError("No such key - {}".format(key))
def set_value(self, key, value):
# type: (str, Any) -> None
"""Modify a value in the configuration.
"""
self._ensure_have_load_only()
fname, parser = self._get_parser_to_modify()
if parser is not None:
section, name = _disassemble_key(key)
# Modify the parser and the configuration
if not parser.has_section(section):
parser.add_section(section)
parser.set(section, name, value)
self._config[self.load_only][key] = value
self._mark_as_modified(fname, parser)
def unset_value(self, key):
# type: (str) -> None
"""Unset a value in the configuration.
"""
self._ensure_have_load_only()
if key not in self._config[self.load_only]:
raise ConfigurationError("No such key - {}".format(key))
fname, parser = self._get_parser_to_modify()
if parser is not None:
section, name = _disassemble_key(key)
# Remove the key in the parser
modified_something = False
if parser.has_section(section):
# Returns whether the option was removed or not
modified_something = parser.remove_option(section, name)
if modified_something:
# name removed from parser, section may now be empty
section_iter = iter(parser.items(section))
try:
val = six.next(section_iter)
except StopIteration:
val = None
if val is None:
parser.remove_section(section)
self._mark_as_modified(fname, parser)
else:
raise ConfigurationError(
"Fatal Internal error [id=1]. Please report as a bug."
)
del self._config[self.load_only][key]
def save(self):
# type: () -> None
"""Save the currentin-memory state.
"""
self._ensure_have_load_only()
for fname, parser in self._modified_parsers:
logger.info("Writing to %s", fname)
# Ensure directory exists.
ensure_dir(os.path.dirname(fname))
with open(fname, "w") as f:
parser.write(f) # type: ignore
#
# Private routines
#
def _ensure_have_load_only(self):
# type: () -> None
if self.load_only is None:
raise ConfigurationError("Needed a specific file to be modifying.")
logger.debug("Will be working with %s variant only", self.load_only)
@property
def _dictionary(self):
# type: () -> Dict[str, Any]
"""A dictionary representing the loaded configuration.
"""
# NOTE: Dictionaries are not populated if not loaded. So, conditionals
# are not needed here.
retval = {}
for variant in self._override_order:
retval.update(self._config[variant])
return retval
def _load_config_files(self):
# type: () -> None
"""Loads configuration from configuration files
"""
config_files = dict(self._iter_config_files())
if config_files[kinds.ENV][0:1] == [os.devnull]:
logger.debug(
"Skipping loading configuration files due to "
"environment's PIP_CONFIG_FILE being os.devnull"
)
return
for variant, files in config_files.items():
for fname in files:
# If there's specific variant set in `load_only`, load only
# that variant, not the others.
if self.load_only is not None and variant != self.load_only:
logger.debug(
"Skipping file '%s' (variant: %s)", fname, variant
)
continue
parser = self._load_file(variant, fname)
# Keeping track of the parsers used
self._parsers[variant].append((fname, parser))
def _load_file(self, variant, fname):
# type: (Kind, str) -> RawConfigParser
logger.debug("For variant '%s', will try loading '%s'", variant, fname)
parser = self._construct_parser(fname)
for section in parser.sections():
items = parser.items(section)
self._config[variant].update(self._normalized_keys(section, items))
return parser
def _construct_parser(self, fname):
# type: (str) -> RawConfigParser
parser = configparser.RawConfigParser()
# If there is no such file, don't bother reading it but create the
# parser anyway, to hold the data.
# Doing this is useful when modifying and saving files, where we don't
# need to construct a parser.
if os.path.exists(fname):
try:
parser.read(fname)
except UnicodeDecodeError:
raise ConfigurationError((
"ERROR: "
"Configuration file contains invalid %s characters.\n"
"Please fix your configuration, located at %s\n"
) % (locale.getpreferredencoding(False), fname))
return parser
def _load_environment_vars(self):
# type: () -> None
"""Loads configuration from environment variables
"""
self._config[kinds.ENV_VAR].update(
self._normalized_keys(":env:", self._get_environ_vars())
)
def _normalized_keys(self, section, items):
# type: (str, Iterable[Tuple[str, Any]]) -> Dict[str, Any]
"""Normalizes items to construct a dictionary with normalized keys.
This routine is where the names become keys and are made the same
regardless of source - configuration files or environment.
"""
normalized = {}
for name, val in items:
key = section + "." + _normalize_name(name)
normalized[key] = val
return normalized
def _get_environ_vars(self):
# type: () -> Iterable[Tuple[str, str]]
"""Returns a generator with all environmental vars with prefix PIP_"""
for key, val in os.environ.items():
should_be_yielded = (
key.startswith("PIP_") and
key[4:].lower() not in self._ignore_env_names
)
if should_be_yielded:
yield key[4:].lower(), val
# XXX: This is patched in the tests.
def _iter_config_files(self):
# type: () -> Iterable[Tuple[Kind, List[str]]]
"""Yields variant and configuration files associated with it.
This should be treated like items of a dictionary.
"""
# SMELL: Move the conditions out of this function
# environment variables have the lowest priority
config_file = os.environ.get('PIP_CONFIG_FILE', None)
if config_file is not None:
yield kinds.ENV, [config_file]
else:
yield kinds.ENV, []
# at the base we have any global configuration
yield kinds.GLOBAL, list(site_config_files)
# per-user configuration next
should_load_user_config = not self.isolated and not (
config_file and os.path.exists(config_file)
)
if should_load_user_config:
# The legacy config file is overridden by the new config file
yield kinds.USER, [legacy_config_file, new_config_file]
# finally virtualenv configuration first trumping others
if running_under_virtualenv():
yield kinds.VENV, [venv_config_file]
def _get_parser_to_modify(self):
# type: () -> Tuple[str, RawConfigParser]
# Determine which parser to modify
parsers = self._parsers[self.load_only]
if not parsers:
# This should not happen if everything works correctly.
raise ConfigurationError(
"Fatal Internal error [id=2]. Please report as a bug."
)
# Use the highest priority parser.
return parsers[-1]
# XXX: This is patched in the tests.
def _mark_as_modified(self, fname, parser):
# type: (str, RawConfigParser) -> None
file_parser_tuple = (fname, parser)
if file_parser_tuple not in self._modified_parsers:
self._modified_parsers.append(file_parser_tuple)
@@ -11,44 +11,49 @@ import platform
import re
import shutil
import sys
import tempfile
from notpip._vendor import requests, six, urllib3
from notpip._vendor.cachecontrol import CacheControlAdapter
from notpip._vendor.cachecontrol.caches import FileCache
from notpip._vendor.lockfile import LockError
from notpip._vendor.requests.adapters import BaseAdapter, HTTPAdapter
from notpip._vendor.requests.auth import AuthBase, HTTPBasicAuth
from notpip._vendor.requests.models import CONTENT_CHUNK_SIZE, Response
from notpip._vendor.requests.structures import CaseInsensitiveDict
from notpip._vendor.requests.utils import get_netrc_auth
# NOTE: XMLRPC Client is not annotated in typeshed as on 2017-07-17, which is
# why we ignore the type on this import
from notpip._vendor.six.moves import xmlrpc_client # type: ignore
from notpip._vendor.six.moves.urllib import parse as urllib_parse
from notpip._vendor.six.moves.urllib import request as urllib_request
from notpip._vendor.six.moves.urllib.parse import unquote as urllib_unquote
from notpip._vendor.urllib3.util import IS_PYOPENSSL
import notpip
from notpip._internal.compat import WINDOWS
from notpip._internal.exceptions import HashMismatch, InstallationError
from notpip._internal.locations import write_delete_marker_file
from notpip._internal.models import PyPI
from notpip._internal.utils.encoding import auto_decode
from notpip._internal.utils.filesystem import check_path_owner
from notpip._internal.utils.glibc import libc_ver
from notpip._internal.utils.logging import indent_log
from notpip._internal.utils.misc import (
ARCHIVE_EXTENSIONS, ask_path_exists, backup_dir, call_subprocess, consume,
display_path, format_size, get_installed_version, rmtree, splitext,
unpack_file,
)
from notpip._internal.utils.setuptools_build import SETUPTOOLS_SHIM
from notpip._internal.utils.temp_dir import TempDirectory
from notpip._internal.utils.ui import DownloadProgressProvider
from notpip._internal.vcs import vcs
try:
import ssl # noqa
HAS_TLS = True
except ImportError:
HAS_TLS = False
from pip9._vendor.six.moves.urllib import parse as urllib_parse
from pip9._vendor.six.moves.urllib import request as urllib_request
import pip9
from pip9.exceptions import InstallationError, HashMismatch
from pip9.models import PyPI
from pip9.utils import (splitext, rmtree, format_size, display_path,
backup_dir, ask_path_exists, unpack_file,
ARCHIVE_EXTENSIONS, consume, call_subprocess)
from pip9.utils.encoding import auto_decode
from pip9.utils.filesystem import check_path_owner
from pip9.utils.logging import indent_log
from pip9.utils.setuptools_build import SETUPTOOLS_SHIM
from pip9.utils.glibc import libc_ver
from pip9.utils.ui import DownloadProgressBar, DownloadProgressSpinner
from pip9.locations import write_delete_marker_file
from pip9.vcs import vcs
from pip9._vendor import requests, six
from pip9._vendor.requests.adapters import BaseAdapter, HTTPAdapter
from pip9._vendor.requests.auth import AuthBase, HTTPBasicAuth
from pip9._vendor.requests.models import CONTENT_CHUNK_SIZE, Response
from pip9._vendor.requests.utils import get_netrc_auth
from pip9._vendor.requests.structures import CaseInsensitiveDict
from pip9._vendor import urllib3
from pip9._vendor.cachecontrol import CacheControlAdapter
from pip9._vendor.cachecontrol.caches import FileCache
from pip9._vendor.lockfile import LockError
from pip9._vendor.six.moves import xmlrpc_client
ssl = None
HAS_TLS = (ssl is not None) or IS_PYOPENSSL
__all__ = ['get_file_content',
'is_url', 'url_to_path', 'path_to_url',
@@ -65,7 +70,7 @@ def user_agent():
Return a string representing the user agent.
"""
data = {
"installer": {"name": "pip", "version": pip9.__version__},
"installer": {"name": "pip", "version": pip.__version__},
"python": platform.python_version(),
"implementation": {
"name": platform.python_implementation(),
@@ -90,7 +95,7 @@ def user_agent():
data["implementation"]["version"] = platform.python_version()
if sys.platform.startswith("linux"):
from pip9._vendor import distro
from notpip._vendor import distro
distro_infos = dict(filter(
lambda x: x[1],
zip(["name", "version", "id"], distro.linux_distribution()),
@@ -116,10 +121,13 @@ def user_agent():
if platform.machine():
data["cpu"] = platform.machine()
# Python 2.6 doesn't have ssl.OPENSSL_VERSION.
if HAS_TLS and sys.version_info[:2] > (2, 6):
if HAS_TLS:
data["openssl_version"] = ssl.OPENSSL_VERSION
setuptools_version = get_installed_version("setuptools")
if setuptools_version is not None:
data["setuptools_version"] = setuptools_version
return "{data[installer][name]}/{data[installer][version]} {json}".format(
data=data,
json=json.dumps(data, separators=(",", ":"), sort_keys=True),
@@ -203,8 +211,9 @@ class MultiDomainBasicAuth(AuthBase):
if "@" in netloc:
userinfo = netloc.rsplit("@", 1)[0]
if ":" in userinfo:
return userinfo.split(":", 1)
return userinfo, None
user, pwd = userinfo.split(":", 1)
return (urllib_unquote(user), urllib_unquote(pwd))
return urllib_unquote(userinfo), None
return None, None
@@ -251,7 +260,7 @@ class SafeFileCache(FileCache):
super(SafeFileCache, self).__init__(*args, **kwargs)
# Check to ensure that the directory containing our cache directory
# is owned by the user current executing pip9. If it does not exist
# 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 it is not owned by the user executing pip then we will disable
# the cache and log a warning.
@@ -342,7 +351,9 @@ class PipSession(requests.Session):
# connection got interrupted in some way. A 503 error in general
# is typically considered a transient error so we'll go ahead and
# retry it.
status_forcelist=[503],
# A 500 may indicate transient error in Amazon S3
# A 520 or 527 - may indicate transient error in CloudFlare
status_forcelist=[500, 503, 520, 527],
# Add a small amount of back off between failed requests in
# order to prevent hammering the service.
@@ -376,7 +387,7 @@ class PipSession(requests.Session):
# We want to use a non-validating adapter for any requests which are
# deemed insecure.
for host in insecure_hosts:
self.mount("https://{0}/".format(host), insecure_adapter)
self.mount("https://{}/".format(host), insecure_adapter)
def request(self, method, url, *args, **kwargs):
# Allow setting a default timeout on a session
@@ -388,7 +399,12 @@ class PipSession(requests.Session):
def get_file_content(url, comes_from=None, session=None):
"""Gets the content of a file; it may be a filename, file: URL, or
http: URL. Returns (location, content). Content is unicode."""
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'"
@@ -509,14 +525,13 @@ def _progress_indicator(iterable, *args, **kwargs):
return iterable
def _download_url(resp, link, content_file, hashes):
def _download_url(resp, link, content_file, hashes, progress_bar):
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:
@@ -580,12 +595,12 @@ def _download_url(resp, link, content_file, hashes):
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))
progress_indicator = DownloadProgressBar(max=total_length).iter
else:
logger.info("Downloading %s", url)
progress_indicator = DownloadProgressSpinner().iter
elif cached_resp:
logger.info("Using cached %s", url)
else:
@@ -633,42 +648,41 @@ def _copy_file(filename, location, link):
def unpack_http_url(link, location, download_dir=None,
session=None, hashes=None):
session=None, hashes=None, progress_bar="on"):
if session is None:
raise TypeError(
"unpack_http_url() missing 1 required keyword argument: 'session'"
)
temp_dir = tempfile.mkdtemp('-unpack', 'pip-')
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 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)
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,
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, link)
# 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, link)
# 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)
# 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)
rmtree(temp_dir)
if not already_downloaded_path:
os.unlink(from_path)
def unpack_file_url(link, location, download_dir=None, hashes=None):
@@ -739,7 +753,7 @@ def _copy_dist_from_dir(link_path, location):
# build an sdist
setup_py = 'setup.py'
sdist_args = [os.environ['PIP_PYTHON_PATH']]
sdist_args = [os.environ.get('PIP_PYTHON_PATH', sys.executable)]
sdist_args.append('-c')
sdist_args.append(SETUPTOOLS_SHIM % setup_py)
sdist_args.append('sdist')
@@ -785,7 +799,8 @@ class PipXmlrpcTransport(xmlrpc_client.Transport):
def unpack_url(link, location, download_dir=None,
only_download=False, session=None, hashes=None):
only_download=False, session=None, hashes=None,
progress_bar="on"):
"""Unpack link.
If link is a VCS link:
if only_download, export into download_dir and ignore location
@@ -818,13 +833,14 @@ def unpack_url(link, location, download_dir=None,
location,
download_dir,
session,
hashes=hashes
hashes=hashes,
progress_bar=progress_bar
)
if only_download:
write_delete_marker_file(location)
def _download_http_url(link, session, temp_dir, hashes):
def _download_http_url(link, session, temp_dir, hashes, progress_bar):
"""Download link url into temp_dir using provided session"""
target_url = link.url.split('#', 1)[0]
try:
@@ -879,7 +895,7 @@ def _download_http_url(link, session, temp_dir, hashes):
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)
_download_url(resp, link, content_file, hashes, progress_bar)
return file_path, content_type
@@ -3,13 +3,17 @@ from __future__ import absolute_import
from itertools import chain, groupby, repeat
from pip9._vendor.six import iteritems
from notpip._vendor.six import iteritems
class PipError(Exception):
"""Base pip exception"""
class ConfigurationError(PipError):
"""General exception in configuration"""
class InstallationError(PipError):
"""General exception during installation"""
@@ -158,7 +162,8 @@ class HashMissing(HashError):
self.gotten_hash = gotten_hash
def body(self):
from pip9.utils.hashes import FAVORITE_HASH # Dodge circular import.
# Dodge circular import.
from notpip._internal.utils.hashes import FAVORITE_HASH
package = None
if self.req:
@@ -1,42 +1,42 @@
"""Routines related to PyPI, indexes"""
from __future__ import absolute_import
import logging
import cgi
from collections import namedtuple
import itertools
import sys
import os
import re
import logging
import mimetypes
import os
import posixpath
import re
import sys
import warnings
from collections import namedtuple
from pip9._vendor.six.moves.urllib import parse as urllib_parse
from pip9._vendor.six.moves.urllib import request as urllib_request
from notpip._vendor import html5lib, requests, six
from notpip._vendor.distlib.compat import unescape
from notpip._vendor.packaging import specifiers
from notpip._vendor.packaging.utils import canonicalize_name
from notpip._vendor.packaging.version import parse as parse_version
from notpip._vendor.requests.exceptions import SSLError
from notpip._vendor.six.moves.urllib import parse as urllib_parse
from notpip._vendor.six.moves.urllib import request as urllib_request
from pip9.compat import ipaddress
from pip9.utils import (
cached_property, splitext, normalize_path,
ARCHIVE_EXTENSIONS, SUPPORTED_EXTENSIONS,
)
from pip9.utils.deprecation import RemovedInPip10Warning
from pip9.utils.logging import indent_log
from notpip.utils.packaging import check_requires_python
from pip9.exceptions import (
DistributionNotFound, BestVersionAlreadyInstalled, InvalidWheelFilename,
from notpip._internal.compat import ipaddress
from notpip._internal.download import HAS_TLS, is_url, path_to_url, url_to_path
from notpip._internal.exceptions import (
BestVersionAlreadyInstalled, DistributionNotFound, InvalidWheelFilename,
UnsupportedWheel,
)
from pip9.download import HAS_TLS, is_url, path_to_url, url_to_path
from notpip.wheel import Wheel, wheel_ext
from pip9.pep425tags import get_supported
from notpip._vendor import html5lib, requests, six
from pip9._vendor.packaging.version import parse as parse_version
from pip9._vendor.packaging.utils import canonicalize_name
from notpip._vendor.packaging import specifiers
from pip9._vendor.requests.exceptions import SSLError, HTTPError
from pip9._vendor.distlib.compat import unescape
from notpip._internal.models import PyPI
from notpip._internal.pep425tags import get_supported
from notpip._internal.utils.deprecation import RemovedInPip11Warning
from notpip._internal.utils.logging import indent_log
from notpip._internal.utils.misc import (
ARCHIVE_EXTENSIONS, SUPPORTED_EXTENSIONS, cached_property, normalize_path,
splitext,
)
from notpip._internal.utils.packaging import check_requires_python
from notpip._internal.wheel import Wheel, wheel_ext
__all__ = ['FormatControl', 'fmt_ctl_handle_mutual_exclude', 'PackageFinder']
@@ -67,8 +67,8 @@ class InstallationCandidate(object):
self.requires_python = requires_python
def __repr__(self):
return "<InstallationCandidate({0!r}, {1!r}, {2!r})>".format(
self.project, self.version, self.location
return "<InstallationCandidate({!r}, {!r}, {!r})>".format(
self.project, self.version, self.location,
)
def __hash__(self):
@@ -193,6 +193,18 @@ class PackageFinder(object):
)
break
def get_formatted_locations(self):
lines = []
if self.index_urls and self.index_urls != [PyPI.simple_url]:
lines.append(
"Looking in indexes: {}".format(", ".join(self.index_urls))
)
if self.find_links:
lines.append(
"Looking in links: {}".format(", ".join(self.find_links))
)
return "\n".join(lines)
def add_dependency_links(self, links):
# # FIXME: this shouldn't be global list this, it should only
# # apply to requirements of the package that specifies the
@@ -202,7 +214,7 @@ class PackageFinder(object):
warnings.warn(
"Dependency Links processing has been deprecated and will be "
"removed in a future release.",
RemovedInPip10Warning,
RemovedInPip11Warning,
)
self.dependency_links.extend(links)
@@ -224,8 +236,6 @@ class PackageFinder(object):
return extras
@staticmethod
def _sort_locations(locations, expand_dir=False):
"""
@@ -265,14 +275,16 @@ class PackageFinder(object):
else:
logger.warning(
"Url '%s' is ignored: it is neither a file "
"nor a directory.", url)
"nor a directory.", url,
)
elif is_url(url):
# Only add url with clear scheme
urls.append(url)
else:
logger.warning(
"Url '%s' is ignored. It is either a non-existing "
"path or lacks a specific scheme.", url)
"path or lacks a specific scheme.", url,
)
return files, urls
@@ -290,6 +302,7 @@ class PackageFinder(object):
with the same version, would have to be considered equal
"""
support_num = len(self.valid_tags)
build_tag = tuple()
if candidate.location.is_wheel:
# can raise InvalidWheelFilename
wheel = Wheel(candidate.location.filename)
@@ -304,9 +317,14 @@ class PackageFinder(object):
pri = -(wheel.support_index_min(tags=tags))
except TypeError:
pri = -(support_num)
if wheel.build_tag is not None:
match = re.match(r'^(\d+)(.*)$', wheel.build_tag)
build_tag_groups = match.groups()
build_tag = (int(build_tag_groups[0]), build_tag_groups[1])
else: # sdist
pri = -(support_num)
return (candidate.version, pri)
return (candidate.version, build_tag, pri)
def _validate_secure_origin(self, logger, location):
# Determine if this url used a secure transport mechanism
@@ -370,9 +388,9 @@ class PackageFinder(object):
# log a warning that we are ignoring it.
logger.warning(
"The repository located at %s is not a trusted or secure host and "
"is being ignored. If this repository is available via HTTPS it "
"is recommended to use HTTPS instead, otherwise you may silence "
"this warning and allow it anyways with '--trusted-host %s'.",
"is being ignored. If this repository is available via HTTPS we "
"recommend you use HTTPS instead, otherwise you may silence "
"this warning and allow it anyway with '--trusted-host %s'.",
parsed.hostname,
parsed.hostname,
)
@@ -412,13 +430,13 @@ class PackageFinder(object):
index_locations = self._get_index_urls_locations(project_name)
index_file_loc, index_url_loc = self._sort_locations(index_locations)
fl_file_loc, fl_url_loc = self._sort_locations(
self.find_links, expand_dir=True)
self.find_links, expand_dir=True,
)
dep_file_loc, dep_url_loc = self._sort_locations(self.dependency_links)
file_locations = (
Link(url) for url in itertools.chain(
index_file_loc, fl_file_loc, dep_file_loc)
)
file_locations = (Link(url) for url in itertools.chain(
index_file_loc, fl_file_loc, dep_file_loc,
))
# We trust every url that the user has given us whether it was given
# via --index-url or --find-links
@@ -513,7 +531,6 @@ class PackageFinder(object):
)
else:
compatible_versions = [str(c.version) for c in all_candidates]
applicable_candidates = [
# Again, converting to str to deal with debundling.
c for c in all_candidates if str(c.version) in compatible_versions
@@ -537,7 +554,7 @@ class PackageFinder(object):
req,
', '.join(
sorted(
set(str(c.version) for c in all_candidates),
{str(c.version) for c in all_candidates},
key=parse_version,
)
)
@@ -600,7 +617,7 @@ class PackageFinder(object):
try:
page = self._get_page(location)
except HTTPError as e:
except requests.HTTPError as e:
page = None
if page is None:
continue
@@ -652,11 +669,13 @@ class PackageFinder(object):
# Always ignore unsupported extensions even when we ignore compatibility
if ext not in SUPPORTED_EXTENSIONS:
self._log_skipped_link(
link, 'unsupported archive format: %s' % ext)
link, 'unsupported archive format: %s' % ext,
)
return
if "binary" not in search.formats and ext == wheel_ext and not ignore_compatibility:
self._log_skipped_link(
link, 'No binaries permitted for %s' % search.supplied)
link, 'No binaries permitted for %s' % search.supplied,
)
return
if "macosx10" in link.path and ext == '.zip' and not ignore_compatibility:
self._log_skipped_link(link, 'macosx10 one')
@@ -682,7 +701,8 @@ class PackageFinder(object):
# This should be up by the search.ok_binary check, but see issue 2700.
if "source" not in search.formats and ext != wheel_ext:
self._log_skipped_link(
link, 'No sources permitted for %s' % search.supplied)
link, 'No sources permitted for %s' % search.supplied,
)
return
if not version:
@@ -713,6 +733,7 @@ class PackageFinder(object):
link, link.requires_python)
return
logger.debug('Found link %s, version: %s', link, version)
return InstallationCandidate(search.supplied, version, link, link.requires_python)
def _get_page(self, link):
@@ -783,7 +804,7 @@ class HTMLPage(object):
url = url.split('#', 1)[0]
# Check for VCS schemes that do not support lookup as web pages.
from pip9.vcs import VcsSupport
from notpip._internal.vcs import VcsSupport
for scheme in VcsSupport.schemes:
if url.lower().startswith(scheme) and url[len(scheme)] in '+:':
logger.debug('Cannot look at %s URL %s', scheme, link)
@@ -848,8 +869,8 @@ class HTMLPage(object):
except requests.HTTPError as exc:
cls._handle_fail(link, exc, url)
except SSLError as exc:
reason = ("There was a problem confirming the ssl certificate: "
"%s" % exc)
reason = "There was a problem confirming the ssl certificate: "
reason += str(exc)
cls._handle_fail(link, reason, url, meth=logger.info)
except requests.ConnectionError as exc:
cls._handle_fail(link, "connection error: %s" % exc, url)
@@ -869,7 +890,7 @@ class HTMLPage(object):
def _get_content_type(url, session):
"""Get the Content-Type of the given url, using a HEAD request"""
scheme, netloc, path, query, fragment = urllib_parse.urlsplit(url)
if scheme not in ('http', 'https'):
if scheme not in {'http', 'https'}:
# FIXME: some warning or something?
# assertion error?
return ''
@@ -1065,7 +1086,7 @@ class Link(object):
Determines if this points to an actual artifact (e.g. a tarball) or if
it points to an "abstract" thing like a path or a VCS location.
"""
from pip9.vcs import vcs
from notpip._internal.vcs import vcs
if self.scheme in vcs.all_schemes:
return False
@@ -1103,7 +1124,7 @@ def fmt_ctl_handle_mutual_exclude(value, target, other):
def fmt_ctl_formats(fmt_ctl, canonical_name):
result = set(["binary", "source"])
result = {"binary", "source"}
if canonical_name in fmt_ctl.only_binary:
result.discard('source')
elif canonical_name in fmt_ctl.no_binary:
@@ -1117,15 +1138,8 @@ def fmt_ctl_formats(fmt_ctl, canonical_name):
def fmt_ctl_no_binary(fmt_ctl):
fmt_ctl_handle_mutual_exclude(
':all:', fmt_ctl.no_binary, fmt_ctl.only_binary)
def fmt_ctl_no_use_wheel(fmt_ctl):
fmt_ctl_no_binary(fmt_ctl)
warnings.warn(
'--no-use-wheel is deprecated and will be removed in the future. '
' Please use --no-binary :all: instead.', RemovedInPip10Warning,
stacklevel=2)
':all:', fmt_ctl.no_binary, fmt_ctl.only_binary,
)
Search = namedtuple('Search', 'supplied canonical formats')
@@ -3,15 +3,15 @@ from __future__ import absolute_import
import os
import os.path
import platform
import site
import sys
import sysconfig
from distutils import sysconfig as distutils_sysconfig
from distutils.command.install import SCHEME_KEYS, install # type: ignore
from distutils import sysconfig
from distutils.command.install import install, SCHEME_KEYS # noqa
from pip9.compat import WINDOWS, expanduser
from pip9.utils import appdirs
from notpip._internal.compat import WINDOWS, expanduser
from notpip._internal.utils import appdirs
# Application Directories
USER_CACHE_DIR = appdirs.user_cache_dir("pip")
@@ -19,7 +19,7 @@ USER_CACHE_DIR = appdirs.user_cache_dir("pip")
DELETE_MARKER_MESSAGE = '''\
This file is placed here by pip to indicate the source was put
here by pip9.
here by pip.
Once this package is successfully installed this source code will be
deleted (unless you remove this file).
@@ -80,8 +80,18 @@ src_prefix = os.path.abspath(src_prefix)
# FIXME doesn't account for venv linked to global site-packages
site_packages = sysconfig.get_python_lib()
user_site = site.USER_SITE
site_packages = sysconfig.get_path("purelib")
# This is because of a bug in PyPy's sysconfig module, see
# https://bitbucket.org/pypy/pypy/issues/2506/sysconfig-returns-incorrect-paths
# for more information.
if platform.python_implementation().lower() == "pypy":
site_packages = distutils_sysconfig.get_python_lib()
try:
# Use getusersitepackages if this is present, as it ensures that the
# value is initialised properly.
user_site = site.getusersitepackages()
except AttributeError:
user_site = site.USER_SITE
user_dir = expanduser('~')
if WINDOWS:
bin_py = os.path.join(sys.prefix, 'Scripts')
@@ -91,7 +101,7 @@ if WINDOWS:
bin_py = os.path.join(sys.prefix, 'bin')
bin_user = os.path.join(user_site, 'bin')
config_basename = 'pip9.ini'
config_basename = 'pip.ini'
legacy_storage_dir = os.path.join(user_dir, 'pip')
legacy_config_file = os.path.join(
@@ -102,14 +112,13 @@ else:
bin_py = os.path.join(sys.prefix, 'bin')
bin_user = os.path.join(user_site, 'bin')
config_basename = 'pip9.conf'
config_basename = 'pip.conf'
legacy_storage_dir = os.path.join(user_dir, '.pip')
legacy_config_file = os.path.join(
legacy_storage_dir,
config_basename,
)
# Forcing to use /usr/local/bin for standard macOS framework installs
# Also log to ~/Library/Logs/ for use with the Console.app log viewer
if sys.platform[:6] == 'darwin' and sys.prefix[:16] == '/System/Library/':
@@ -120,6 +129,9 @@ site_config_files = [
for path in appdirs.site_config_dirs('pip')
]
venv_config_file = os.path.join(sys.prefix, config_basename)
new_config_file = os.path.join(appdirs.user_config_dir("pip"), config_basename)
def distutils_scheme(dist_name, user=False, home=None, root=None,
isolated=False, prefix=None):
@@ -143,7 +155,7 @@ def distutils_scheme(dist_name, user=False, home=None, root=None,
# 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.
assert not (user and prefix), "user={0} prefix={1}".format(user, prefix)
assert not (user and prefix), "user={} prefix={}".format(user, prefix)
i.user = user or i.user
if user:
i.prefix = ""
@@ -0,0 +1,4 @@
from notpip._internal.models.index import Index, PyPI
__all__ = ["Index", "PyPI"]
@@ -1,4 +1,4 @@
from pip9._vendor.six.moves.urllib import parse as urllib_parse
from notpip._vendor.six.moves.urllib import parse as urllib_parse
class Index(object):
@@ -7,7 +7,6 @@ class Index(object):
self.netloc = urllib_parse.urlsplit(url).netloc
self.simple_url = self.url_to_path('simple')
self.pypi_url = self.url_to_path('pypi')
self.pip_json_url = self.url_to_path('pypi/pip/json')
def url_to_path(self, path):
return urllib_parse.urljoin(self.url, path)
@@ -0,0 +1,106 @@
"""Validation of dependencies of packages
"""
from collections import namedtuple
from notpip._vendor.packaging.utils import canonicalize_name
from notpip._internal.operations.prepare import make_abstract_dist
from notpip._internal.utils.misc import get_installed_distributions
from notpip._internal.utils.typing import MYPY_CHECK_RUNNING
if MYPY_CHECK_RUNNING:
from notpip._internal.req.req_install import InstallRequirement
from typing import Any, Dict, Iterator, Set, Tuple, List
# Shorthands
PackageSet = Dict[str, 'PackageDetails']
Missing = Tuple[str, Any]
Conflicting = Tuple[str, str, Any]
MissingDict = Dict[str, List[Missing]]
ConflictingDict = Dict[str, List[Conflicting]]
CheckResult = Tuple[MissingDict, ConflictingDict]
PackageDetails = namedtuple('PackageDetails', ['version', 'requires'])
def create_package_set_from_installed(**kwargs):
# type: (**Any) -> PackageSet
"""Converts a list of distributions into a PackageSet.
"""
# Default to using all packages installed on the system
if kwargs == {}:
kwargs = {"local_only": False, "skip": ()}
retval = {}
for dist in get_installed_distributions(**kwargs):
name = canonicalize_name(dist.project_name)
retval[name] = PackageDetails(dist.version, dist.requires())
return retval
def check_package_set(package_set):
# type: (PackageSet) -> CheckResult
"""Check if a package set is consistent
"""
missing = dict()
conflicting = dict()
for package_name in package_set:
# Info about dependencies of package_name
missing_deps = set() # type: Set[Missing]
conflicting_deps = set() # type: Set[Conflicting]
for req in package_set[package_name].requires:
name = canonicalize_name(req.project_name) # type: str
# Check if it's missing
if name not in package_set:
missed = True
if req.marker is not None:
missed = req.marker.evaluate()
if missed:
missing_deps.add((name, req))
continue
# Check if there's a conflict
version = package_set[name].version # type: str
if not req.specifier.contains(version, prereleases=True):
conflicting_deps.add((name, version, req))
def str_key(x):
return str(x)
if missing_deps:
missing[package_name] = sorted(missing_deps, key=str_key)
if conflicting_deps:
conflicting[package_name] = sorted(conflicting_deps, key=str_key)
return missing, conflicting
def check_install_conflicts(to_install):
# type: (List[InstallRequirement]) -> Tuple[PackageSet, CheckResult]
"""For checking if the dependency graph would be consistent after \
installing given requirements
"""
# Start from the current state
state = create_package_set_from_installed()
_simulate_installation_of(to_install, state)
return state, check_package_set(state)
# NOTE from @pradyunsg
# This required a minor update in dependency link handling logic over at
# operations.prepare.IsSDist.dist() to get it working
def _simulate_installation_of(to_install, state):
# type: (List[InstallRequirement], PackageSet) -> None
"""Computes the version of packages after installing to_install.
"""
# Modify it as installing requirement_set would (assuming no errors)
for inst_req in to_install:
dist = make_abstract_dist(inst_req).dist(finder=None)
name = canonicalize_name(dist.key)
state[name] = PackageDetails(dist.version, dist.requires())
@@ -0,0 +1,252 @@
from __future__ import absolute_import
import collections
import logging
import os
import re
import warnings
from notpip._vendor import pkg_resources, six
from notpip._vendor.packaging.utils import canonicalize_name
from notpip._vendor.pkg_resources import RequirementParseError
from notpip._internal.exceptions import InstallationError
from notpip._internal.req import InstallRequirement
from notpip._internal.req.req_file import COMMENT_RE
from notpip._internal.utils.deprecation import RemovedInPip11Warning
from notpip._internal.utils.misc import (
dist_is_editable, get_installed_distributions,
)
logger = logging.getLogger(__name__)
def freeze(
requirement=None,
find_links=None, local_only=None, user_only=None, skip_regex=None,
isolated=False,
wheel_cache=None,
exclude_editable=False,
skip=()):
find_links = find_links or []
skip_match = None
if skip_regex:
skip_match = re.compile(skip_regex).search
dependency_links = []
for dist in pkg_resources.working_set:
if dist.has_metadata('dependency_links.txt'):
dependency_links.extend(
dist.get_metadata_lines('dependency_links.txt')
)
for link in find_links:
if '#egg=' in link:
dependency_links.append(link)
for link in find_links:
yield '-f %s' % link
installations = {}
for dist in get_installed_distributions(local_only=local_only,
skip=(),
user_only=user_only):
try:
req = FrozenRequirement.from_dist(
dist,
dependency_links
)
except RequirementParseError:
logger.warning(
"Could not parse requirement: %s",
dist.project_name
)
continue
if exclude_editable and req.editable:
continue
installations[req.name] = req
if requirement:
# the options that don't get turned into an InstallRequirement
# should only be emitted once, even if the same option is in multiple
# requirements files, so we need to keep track of what has been emitted
# so that we don't emit it again if it's seen again
emitted_options = set()
# keep track of which files a requirement is in so that we can
# give an accurate warning if a requirement appears multiple times.
req_files = collections.defaultdict(list)
for req_file_path in requirement:
with open(req_file_path) as req_file:
for line in req_file:
if (not line.strip() or
line.strip().startswith('#') or
(skip_match and skip_match(line)) or
line.startswith((
'-r', '--requirement',
'-Z', '--always-unzip',
'-f', '--find-links',
'-i', '--index-url',
'--pre',
'--trusted-host',
'--process-dependency-links',
'--extra-index-url'))):
line = line.rstrip()
if line not in emitted_options:
emitted_options.add(line)
yield line
continue
if line.startswith('-e') or line.startswith('--editable'):
if line.startswith('-e'):
line = line[2:].strip()
else:
line = line[len('--editable'):].strip().lstrip('=')
line_req = InstallRequirement.from_editable(
line,
isolated=isolated,
wheel_cache=wheel_cache,
)
else:
line_req = InstallRequirement.from_line(
COMMENT_RE.sub('', line).strip(),
isolated=isolated,
wheel_cache=wheel_cache,
)
if not line_req.name:
logger.info(
"Skipping line in requirement file [%s] because "
"it's not clear what it would install: %s",
req_file_path, line.strip(),
)
logger.info(
" (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 that "
"package is not installed",
req_file_path,
COMMENT_RE.sub('', line).strip(),
)
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)
# Warn about requirements that were included multiple times (in a
# single requirements file or in different requirements files).
for name, files in six.iteritems(req_files):
if len(files) > 1:
logger.warning("Requirement %s included multiple times [%s]",
name, ', '.join(sorted(set(files))))
yield(
'## The following requirements were added by '
'pip freeze:'
)
for installation in sorted(
installations.values(), key=lambda x: x.name.lower()):
if canonicalize_name(installation.name) not in skip:
yield str(installation).rstrip()
class FrozenRequirement(object):
def __init__(self, name, req, editable, comments=()):
self.name = name
self.req = req
self.editable = editable
self.comments = comments
_rev_re = re.compile(r'-r(\d+)$')
_date_re = re.compile(r'-(20\d\d\d\d\d\d)$')
@classmethod
def from_dist(cls, dist, dependency_links):
location = os.path.normcase(os.path.abspath(dist.location))
comments = []
from notpip._internal.vcs import vcs, get_src_requirement
if dist_is_editable(dist) and vcs.get_backend_name(location):
editable = True
try:
req = get_src_requirement(dist, location)
except InstallationError as exc:
logger.warning(
"Error when trying to get requirement for VCS system %s, "
"falling back to uneditable format", exc
)
req = None
if req is None:
logger.warning(
'Could not determine repository location of %s', location
)
comments.append(
'## !! Could not determine repository location'
)
req = dist.as_requirement()
editable = False
else:
editable = False
req = dist.as_requirement()
specs = req.specs
assert len(specs) == 1 and specs[0][0] in ["==", "==="], \
'Expected 1 spec with == or ===; specs = %r; dist = %r' % \
(specs, dist)
version = specs[0][1]
ver_match = cls._rev_re.search(version)
date_match = cls._date_re.search(version)
if ver_match or date_match:
svn_backend = vcs.get_backend('svn')
if svn_backend:
svn_location = svn_backend().get_location(
dist,
dependency_links,
)
if not svn_location:
logger.warning(
'Warning: cannot find svn location for %s', req,
)
comments.append(
'## FIXME: could not find svn URL in dependency_links '
'for this package:'
)
else:
warnings.warn(
"SVN editable detection based on dependency links "
"will be dropped in the future.",
RemovedInPip11Warning,
)
comments.append(
'# Installing as editable to satisfy requirement %s:' %
req
)
if ver_match:
rev = ver_match.group(1)
else:
rev = '{%s}' % date_match.group(1)
editable = True
req = '%s@%s#egg=%s' % (
svn_location,
rev,
cls.egg_name(dist)
)
return cls(dist.project_name, req, editable, comments)
@staticmethod
def egg_name(dist):
name = dist.egg_name()
match = re.search(r'-py\d\.\d$', name)
if match:
name = name[:match.start()]
return name
def __str__(self):
req = self.req
if self.editable:
req = '-e %s' % req
return '\n'.join(list(self.comments) + [str(req)]) + '\n'
@@ -0,0 +1,380 @@
"""Prepares a distribution for installation
"""
import itertools
import logging
import os
import sys
from copy import copy
from notpip._vendor import pkg_resources, requests
from notpip._internal.build_env import NoOpBuildEnvironment
from notpip._internal.compat import expanduser
from notpip._internal.download import (
is_dir_url, is_file_url, is_vcs_url, unpack_url, url_to_path,
)
from notpip._internal.exceptions import (
DirectoryUrlHashUnsupported, HashUnpinned, InstallationError,
PreviousBuildDirError, VcsHashUnsupported,
)
from notpip._internal.index import FormatControl
from notpip._internal.req.req_install import InstallRequirement
from notpip._internal.utils.hashes import MissingHashes
from notpip._internal.utils.logging import indent_log
from notpip._internal.utils.misc import (
call_subprocess, display_path, normalize_path,
)
from notpip._internal.utils.ui import open_spinner
from notpip._internal.vcs import vcs
logger = logging.getLogger(__name__)
def make_abstract_dist(req):
"""Factory to make an abstract dist object.
Preconditions: Either an editable req with a source_dir, or satisfied_by or
a wheel link, or a non-editable req with a source_dir.
:return: A concrete DistAbstraction.
"""
if req.editable:
return IsSDist(req)
elif req.link and req.link.is_wheel:
return IsWheel(req)
else:
return IsSDist(req)
def _install_build_reqs(finder, prefix, build_requirements):
# NOTE: What follows is not a very good thing.
# Eventually, this should move into the BuildEnvironment class and
# that should handle all the isolation and sub-process invocation.
finder = copy(finder)
finder.format_control = FormatControl(set(), set([":all:"]))
urls = [
finder.find_requirement(
InstallRequirement.from_line(r), upgrade=False).url
for r in build_requirements
]
args = [
sys.executable, '-m', 'pip', 'install', '--ignore-installed',
'--no-user', '--prefix', prefix,
] + list(urls)
with open_spinner("Installing build dependencies") as spinner:
call_subprocess(args, show_stdout=False, spinner=spinner)
class DistAbstraction(object):
"""Abstracts out the wheel vs non-wheel Resolver.resolve() logic.
The requirements for anything installable are as follows:
- we must be able to determine the requirement name
(or we can't correctly handle the non-upgrade case).
- we must be able to generate a list of run-time dependencies
without installing any additional packages (or we would
have to either burn time by doing temporary isolated installs
or alternatively violate pips 'don't start installing unless
all requirements are available' rule - neither of which are
desirable).
- for packages with setup requirements, we must also be able
to determine their requirements without installing additional
packages (for the same reason as run-time dependencies)
- we must be able to create a Distribution object exposing the
above metadata.
"""
def __init__(self, req):
self.req = req
def dist(self, finder):
"""Return a setuptools Dist object."""
raise NotImplementedError(self.dist)
def prep_for_dist(self, finder):
"""Ensure that we can get a Dist for this requirement."""
raise NotImplementedError(self.dist)
class IsWheel(DistAbstraction):
def dist(self, finder):
return list(pkg_resources.find_distributions(
self.req.source_dir))[0]
def prep_for_dist(self, finder, build_isolation):
# FIXME:https://github.com/pypa/pip/issues/1112
pass
class IsSDist(DistAbstraction):
def dist(self, finder):
dist = self.req.get_dist()
# FIXME: shouldn't be globally added.
if finder and dist.has_metadata('dependency_links.txt'):
finder.add_dependency_links(
dist.get_metadata_lines('dependency_links.txt')
)
return dist
def prep_for_dist(self, finder, build_isolation):
# Before calling "setup.py egg_info", we need to set-up the build
# environment.
build_requirements, isolate = self.req.get_pep_518_info()
should_isolate = build_isolation and isolate
minimum_requirements = ('setuptools', 'wheel')
missing_requirements = set(minimum_requirements) - set(
pkg_resources.Requirement(r).key
for r in build_requirements
)
if missing_requirements:
def format_reqs(rs):
return ' and '.join(map(repr, sorted(rs)))
logger.warning(
"Missing build time requirements in pyproject.toml for %s: "
"%s.", self.req, format_reqs(missing_requirements)
)
logger.warning(
"This version of pip does not implement PEP 517 so it cannot "
"build a wheel without %s.", format_reqs(minimum_requirements)
)
if should_isolate:
with self.req.build_env:
pass
_install_build_reqs(finder, self.req.build_env.path,
build_requirements)
else:
self.req.build_env = NoOpBuildEnvironment(no_clean=False)
self.req.run_egg_info()
self.req.assert_source_matches_version()
class Installed(DistAbstraction):
def dist(self, finder):
return self.req.satisfied_by
def prep_for_dist(self, finder):
pass
class RequirementPreparer(object):
"""Prepares a Requirement
"""
def __init__(self, build_dir, download_dir, src_dir, wheel_download_dir,
progress_bar, build_isolation):
super(RequirementPreparer, self).__init__()
self.src_dir = src_dir
self.build_dir = build_dir
# Where still packed archives should be written to. If None, they are
# not saved, and are deleted immediately after unpacking.
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
# download_dir and wheel_download_dir overlap semantically and may
# 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
@property
def _download_should_save(self):
# TODO: Modify to reduce indentation needed
if self.download_dir:
self.download_dir = expanduser(self.download_dir)
if os.path.exists(self.download_dir):
return True
else:
logger.critical('Could not find download directory')
raise InstallationError(
"Could not find or access download directory '%s'"
% display_path(self.download_dir))
return False
def prepare_linked_requirement(self, req, session, finder,
upgrade_allowed, require_hashes):
"""Prepare a requirement that would be obtained from req.link
"""
# TODO: Breakup into smaller functions
if req.link and req.link.scheme == 'file':
path = url_to_path(req.link.url)
logger.info('Processing %s', display_path(path))
else:
logger.info('Collecting %s', req)
with indent_log():
# @@ if filesystem packages are not marked
# editable in a req, a non deterministic error
# occurs when the script attempts to unpack the
# build directory
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
# installation.
# FIXME: this won't upgrade when there's an existing
# package unpacked in `req.source_dir`
# package unpacked in `req.source_dir`
if os.path.exists(os.path.join(req.source_dir, 'setup.py')):
raise PreviousBuildDirError(
"pip can't proceed with requirements '%s' due to a"
" pre-existing build directory (%s). This is "
"likely due to a previous installation that failed"
". pip is being responsible and not assuming it "
"can delete this. Please delete it and try again."
% (req, req.source_dir)
)
req.populate_link(finder, upgrade_allowed, require_hashes)
# We can't hit this spot and have populate_link return None.
# req.satisfied_by is None here (because we're
# guarded) and upgrade has no impact except when satisfied_by
# is not None.
# Then inside find_requirement existing_applicable -> False
# If no new versions are found, DistributionNotFound is raised,
# otherwise a result is guaranteed.
assert req.link
link = req.link
# Now that we have the real link, we can tell what kind of
# 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:
# 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
# unhashable requirements, complaining that there's no
# hash provided.
if is_vcs_url(link):
raise VcsHashUnsupported()
elif is_file_url(link) and is_dir_url(link):
raise DirectoryUrlHashUnsupported()
if not req.original_link and not req.is_pinned:
# Unpinned packages are asking for trouble when a new
# version is uploaded. This isn't a security check, but
# it saves users a surprising hash mismatch in the
# future.
#
# file:/// URLs aren't pinnable, so don't complain
# about them not being pinned.
raise HashUnpinned()
hashes = req.hashes(trust_internet=not require_hashes)
if 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
# showing the user what the hash should be.
hashes = MissingHashes()
try:
download_dir = self.download_dir
# We always delete unpacked sdists after pip ran.
autodelete_unpacked = True
if req.link.is_wheel and self.wheel_download_dir:
# when doing 'pip wheel` we download wheels to a
# dedicated dir.
download_dir = self.wheel_download_dir
if req.link.is_wheel:
if download_dir:
# When downloading, we only unpack wheels to get
# metadata.
autodelete_unpacked = True
else:
# When installing a wheel, we use the unpacked
# wheel.
autodelete_unpacked = False
unpack_url(
req.link, req.source_dir,
download_dir, autodelete_unpacked,
session=session, hashes=hashes,
progress_bar=self.progress_bar
)
except requests.HTTPError as exc:
logger.critical(
'Could not install requirement %s because of error %s',
req,
exc,
)
raise InstallationError(
'Could not install requirement %s because of HTTP '
'error %s for URL %s' %
(req, exc, req.link)
)
abstract_dist = make_abstract_dist(req)
abstract_dist.prep_for_dist(finder, self.build_isolation)
if self._download_should_save:
# Make a .zip of the source_dir we already created.
if req.link.scheme in vcs.all_schemes:
req.archive(self.download_dir)
return abstract_dist
def prepare_editable_requirement(self, req, require_hashes, use_user_site,
finder):
"""Prepare an editable requirement
"""
assert req.editable, "cannot prepare a non-editable req as editable"
logger.info('Obtaining %s', req)
with indent_log():
if require_hashes:
raise InstallationError(
'The editable requirement %s cannot be installed when '
'requiring hashes, because there is no single file to '
'hash.' % req
)
req.ensure_has_source_dir(self.src_dir)
req.update_editable(not self._download_should_save)
abstract_dist = make_abstract_dist(req)
abstract_dist.prep_for_dist(finder, self.build_isolation)
if self._download_should_save:
req.archive(self.download_dir)
req.check_if_exists(use_user_site)
return abstract_dist
def prepare_installed_requirement(self, req, require_hashes, skip_reason):
"""Prepare an already-installed requirement
"""
assert req.satisfied_by, "req should have been satisfied but isn't"
assert skip_reason is not None, (
"did not get skip reason skipped but req.satisfied_by "
"is set to %r" % (req.satisfied_by,)
)
logger.info(
'Requirement %s: %s (%s)',
skip_reason, req, req.satisfied_by.version
)
with indent_log():
if require_hashes:
logger.debug(
'Since it is already installed, we are trusting this '
'package without checking its hash. To ensure a '
'completely repeatable environment, install into an '
'empty virtualenv.'
)
abstract_dist = Installed(req)
return abstract_dist
@@ -1,21 +1,16 @@
"""Generate and work with PEP 425 Compatibility Tags."""
from __future__ import absolute_import
import distutils.util
import logging
import platform
import re
import sys
import sysconfig
import warnings
import platform
import logging
from collections import OrderedDict
try:
import sysconfig
except ImportError: # pragma nocover
# Python < 2.7
import distutils.sysconfig as sysconfig
import distutils.util
from pip9.compat import OrderedDict
import pip9.utils.glibc
import notpip._internal.utils.glibc
logger = logging.getLogger(__name__)
@@ -26,7 +21,7 @@ def get_config_var(var):
try:
return sysconfig.get_config_var(var)
except IOError as e: # Issue #1074
warnings.warn("{0}".format(e), RuntimeWarning)
warnings.warn("{}".format(e), RuntimeWarning)
return None
@@ -66,7 +61,7 @@ def get_impl_tag():
"""
Returns the Tag for this specific implementation.
"""
return "{0}{1}".format(get_abbr_impl(), get_impl_ver())
return "{}{}".format(get_abbr_impl(), get_impl_ver())
def get_flag(var, fallback, expected=True, warn=True):
@@ -86,7 +81,7 @@ def get_abi_tag():
(CPython 2, PyPy)."""
soabi = get_config_var('SOABI')
impl = get_abbr_impl()
if not soabi and impl in ('cp', 'pp') and hasattr(sys, 'maxunicode'):
if not soabi and impl in {'cp', 'pp'} and hasattr(sys, 'maxunicode'):
d = ''
m = ''
u = ''
@@ -133,7 +128,7 @@ def get_platform():
elif machine == "ppc64" and _is_running_32bit():
machine = "ppc"
return 'macosx_{0}_{1}_{2}'.format(split_ver[0], split_ver[1], machine)
return 'macosx_{}_{}_{}'.format(split_ver[0], split_ver[1], machine)
# XXX remove distutils dependency
result = distutils.util.get_platform().replace('.', '_').replace('-', '_')
@@ -147,7 +142,7 @@ def get_platform():
def is_manylinux1_compatible():
# Only Linux, and only x86-64 / i686
if get_platform() not in ("linux_x86_64", "linux_i686"):
if get_platform() not in {"linux_x86_64", "linux_i686"}:
return False
# Check for presence of _manylinux module
@@ -159,7 +154,7 @@ def is_manylinux1_compatible():
pass
# Check glibc version. CentOS 5 uses glibc 2.5.
return pip9.utils.glibc.have_compatible_glibc(2, 5)
return pip._internal.utils.glibc.have_compatible_glibc(2, 5)
def get_darwin_arches(major, minor, machine):
@@ -273,7 +268,7 @@ def get_supported(versions=None, noarch=False, platform=None,
match = _osx_arch_pat.match(arch)
if match:
name, major, minor, actual_arch = match.groups()
tpl = '{0}_{1}_%i_%s'.format(name, major)
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):
@@ -294,7 +289,7 @@ def get_supported(versions=None, noarch=False, platform=None,
# 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'):
if version in {'31', '30'}:
break
for abi in abi3s: # empty set if not Python 3
for arch in arches:
@@ -318,7 +313,5 @@ def get_supported(versions=None, noarch=False, platform=None,
return supported
supported_tags = get_supported()
supported_tags_noarch = get_supported(noarch=True)
implementation_tag = get_impl_tag()
@@ -0,0 +1,69 @@
from __future__ import absolute_import
import logging
from .req_install import InstallRequirement
from .req_set import RequirementSet
from .req_file import parse_requirements
from notpip._internal.utils.logging import indent_log
__all__ = [
"RequirementSet", "InstallRequirement",
"parse_requirements", "install_given_reqs",
]
logger = logging.getLogger(__name__)
def install_given_reqs(to_install, install_options, global_options=(),
*args, **kwargs):
"""
Install everything in the given list.
(to be called after having downloaded and unpacked the packages)
"""
if to_install:
logger.info(
'Installing collected packages: %s',
', '.join([req.name for req in to_install]),
)
with indent_log():
for requirement in to_install:
if requirement.conflicts_with:
logger.info(
'Found existing installation: %s',
requirement.conflicts_with,
)
with indent_log():
uninstalled_pathset = requirement.uninstall(
auto_confirm=True
)
try:
requirement.install(
install_options,
global_options,
*args,
**kwargs
)
except:
should_rollback = (
requirement.conflicts_with and
not requirement.install_succeeded
)
# if install did not succeed, rollback previous uninstall
if should_rollback:
uninstalled_pathset.rollback()
raise
else:
should_commit = (
requirement.conflicts_with and
requirement.install_succeeded
)
if should_commit:
uninstalled_pathset.commit()
requirement.remove_temporary_source()
return to_install
@@ -4,28 +4,31 @@ Requirements file parsing
from __future__ import absolute_import
import optparse
import os
import re
import shlex
import sys
import optparse
import warnings
from pip9._vendor.six.moves.urllib import parse as urllib_parse
from pip9._vendor.six.moves import filterfalse
from notpip._vendor.six.moves import filterfalse
from notpip._vendor.six.moves.urllib import parse as urllib_parse
import pip9
from pip9.download import get_file_content
from pip9.req.req_install import InstallRequirement
from pip9.exceptions import (RequirementsFileParseError)
from pip9.utils.deprecation import RemovedInPip10Warning
from pip9 import cmdoptions
from notpip._internal import cmdoptions
from notpip._internal.download import get_file_content
from notpip._internal.exceptions import RequirementsFileParseError
from notpip._internal.req.req_install import InstallRequirement
__all__ = ['parse_requirements']
SCHEME_RE = re.compile(r'^(http|https|file):', re.I)
COMMENT_RE = re.compile(r'(^|\s)+#.*$')
# Matches environment variable-style values in '${MY_VARIABLE_1}' with the
# variable name consisting of only uppercase letters, digits or the '_'
# (underscore). This follows the POSIX standard defined in IEEE Std 1003.1,
# 2013 Edition.
ENV_VAR_RE = re.compile(r'(?P<var>\$\{(?P<name>[A-Z0-9_]+)\})')
SUPPORTED_OPTIONS = [
cmdoptions.constraints,
cmdoptions.editable,
@@ -34,13 +37,6 @@ SUPPORTED_OPTIONS = [
cmdoptions.index_url,
cmdoptions.find_links,
cmdoptions.extra_index_url,
cmdoptions.allow_external,
cmdoptions.allow_all_external,
cmdoptions.no_allow_external,
cmdoptions.allow_unsafe,
cmdoptions.no_allow_unsafe,
cmdoptions.use_wheel,
cmdoptions.no_use_wheel,
cmdoptions.always_unzip,
cmdoptions.no_binary,
cmdoptions.only_binary,
@@ -66,13 +62,13 @@ def parse_requirements(filename, finder=None, comes_from=None, options=None,
"""Parse a requirements file and yield InstallRequirement instances.
:param filename: Path or url of requirements file.
:param finder: Instance of pip9.index.PackageFinder.
:param finder: Instance of pip.index.PackageFinder.
:param comes_from: Origin description of requirements.
:param options: cli options.
:param session: Instance of pip9.download.PipSession.
:param session: Instance of pip.download.PipSession.
:param constraint: If true, parsing a constraint file rather than
requirements file.
:param wheel_cache: Instance of pip9.wheel.WheelCache
:param wheel_cache: Instance of pip.wheel.WheelCache
"""
if session is None:
raise TypeError(
@@ -104,6 +100,7 @@ def preprocess(content, options):
lines_enum = join_lines(lines_enum)
lines_enum = ignore_comments(lines_enum)
lines_enum = skip_regex(lines_enum, options)
lines_enum = expand_env_variables(lines_enum)
return lines_enum
@@ -127,7 +124,7 @@ def process_line(line, filename, line_number, finder=None, comes_from=None,
:param constraint: If True, parsing a constraints file.
:param options: OptionParser options that we may update
"""
parser = build_parser()
parser = build_parser(line)
defaults = parser.get_default_values()
defaults.index_url = None
if finder:
@@ -141,7 +138,8 @@ def process_line(line, filename, line_number, finder=None, comes_from=None,
# preserve for the nested code path
line_comes_from = '%s %s (line %s)' % (
'-c' if constraint else '-r', filename, line_number)
'-c' if constraint else '-r', filename, line_number,
)
# yield a line requirement
if args_str:
@@ -161,11 +159,9 @@ def process_line(line, filename, line_number, finder=None, comes_from=None,
# yield an editable requirement
elif opts.editables:
isolated = options.isolated_mode if options else False
default_vcs = options.default_vcs if options else None
yield InstallRequirement.from_editable(
opts.editables[0], comes_from=line_comes_from,
constraint=constraint, default_vcs=default_vcs, isolated=isolated,
wheel_cache=wheel_cache
constraint=constraint, isolated=isolated, wheel_cache=wheel_cache
)
# parse a nested requirements file
@@ -198,35 +194,8 @@ def process_line(line, filename, line_number, finder=None, comes_from=None,
# set finder options
elif finder:
if opts.allow_external:
warnings.warn(
"--allow-external has been deprecated and will be removed in "
"the future. Due to changes in the repository protocol, it no "
"longer has any effect.",
RemovedInPip10Warning,
)
if opts.allow_all_external:
warnings.warn(
"--allow-all-external has been deprecated and will be removed "
"in the future. Due to changes in the repository protocol, it "
"no longer has any effect.",
RemovedInPip10Warning,
)
if opts.allow_unverified:
warnings.warn(
"--allow-unverified has been deprecated and will be removed "
"in the future. Due to changes in the repository protocol, it "
"no longer has any effect.",
RemovedInPip10Warning,
)
if opts.index_url:
finder.index_urls = [opts.index_url]
if opts.use_wheel is False:
finder.use_wheel = False
pip9.index.fmt_ctl_no_use_wheel(finder.format_control)
if opts.no_index is True:
finder.index_urls = []
if opts.extra_index_urls:
@@ -267,7 +236,7 @@ def break_args_options(line):
return ' '.join(args), ' '.join(options)
def build_parser():
def build_parser(line):
"""
Return a parser for parsing requirement lines
"""
@@ -281,6 +250,8 @@ def build_parser():
# By default optparse sys.exits on parsing errors. We want to wrap
# that in our own exception.
def parser_exit(self, msg):
# add offending line
msg = 'Invalid requirement: %s\n%s' % (line, msg)
raise RequirementsFileParseError(msg)
parser.exit = parser_exit
@@ -336,7 +307,32 @@ def skip_regex(lines_enum, options):
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)
lines_enum = filterfalse(lambda e: pattern.search(e[1]), lines_enum)
return lines_enum
def expand_env_variables(lines_enum):
"""Replace all environment variables that can be retrieved via `os.getenv`.
The only allowed format for environment variables defined in the
requirement file is `${MY_VARIABLE_1}` to ensure two things:
1. Strings that contain a `$` aren't accidentally (partially) expanded.
2. Ensure consistency across platforms for requirement files.
These points are the result of a discusssion on the `github pull
request #3514 <https://github.com/pypa/pip/pull/3514>`_.
Valid characters in variable names follow the `POSIX standard
<http://pubs.opengroup.org/onlinepubs/9699919799/>`_ and are limited
to uppercase letter, digits and the `_` (underscore).
"""
for line_number, line in lines_enum:
for env_var, var_name in ENV_VAR_RE.findall(line):
value = os.getenv(var_name)
if not value:
continue
line = line.replace(env_var, value)
yield line_number, line
@@ -5,54 +5,52 @@ import os
import re
import shutil
import sys
import tempfile
import sysconfig
import traceback
import warnings
import zipfile
from distutils import sysconfig
from distutils.util import change_root
from email.parser import FeedParser
from email.parser import FeedParser # type: ignore
from notpip._vendor import pkg_resources, six
from notpip._vendor import pkg_resources, pytoml, six
from notpip._vendor.packaging import specifiers
from notpip._vendor.packaging.markers import Marker
from pip9._vendor.packaging.requirements import InvalidRequirement, Requirement
from pip9._vendor.packaging.utils import canonicalize_name
from pip9._vendor.packaging.version import Version, parse as parse_version
from pip9._vendor.six.moves import configparser
from notpip._vendor.packaging.requirements import InvalidRequirement, Requirement
from notpip._vendor.packaging.utils import canonicalize_name
from notpip._vendor.packaging.version import parse as parse_version
from notpip._vendor.packaging.version import Version
from notpip._vendor.pkg_resources import RequirementParseError, parse_requirements
import pip9.wheel
from pip9.compat import native_str, get_stdlib, WINDOWS
from pip9.download import is_url, url_to_path, path_to_url, is_archive_file
from pip9.exceptions import (
InstallationError, UninstallationError,
from notpip._internal import wheel
from notpip._internal.build_env import BuildEnvironment
from notpip._internal.compat import native_str
from notpip._internal.download import (
is_archive_file, is_url, path_to_url, url_to_path,
)
from pip9.locations import (
bin_py, running_under_virtualenv, PIP_DELETE_MARKER_FILENAME, bin_user,
from notpip._internal.exceptions import InstallationError, UninstallationError
from notpip._internal.locations import (
PIP_DELETE_MARKER_FILENAME, running_under_virtualenv,
)
from pip9.utils import (
display_path, rmtree, ask_path_exists, backup_dir, is_installable_dir,
dist_in_usersite, dist_in_site_packages, egg_link_path,
call_subprocess, read_text_file, FakeFile, _make_build_dir, ensure_dir,
get_installed_version, normalize_path, dist_is_local,
from notpip._internal.req.req_uninstall import UninstallPathSet
from notpip._internal.utils.deprecation import RemovedInPip11Warning
from notpip._internal.utils.hashes import Hashes
from notpip._internal.utils.logging import indent_log
from notpip._internal.utils.misc import (
_make_build_dir, ask_path_exists, backup_dir, call_subprocess,
display_path, dist_in_site_packages, dist_in_usersite, ensure_dir,
get_installed_version, is_installable_dir, read_text_file, rmtree,
)
from pip9.utils.hashes import Hashes
from pip9.utils.deprecation import RemovedInPip10Warning
from pip9.utils.logging import indent_log
from pip9.utils.setuptools_build import SETUPTOOLS_SHIM
from pip9.utils.ui import open_spinner
from pip9.req.req_uninstall import UninstallPathSet
from pip9.vcs import vcs
from pip9.wheel import move_wheel_files, Wheel
from notpip._internal.utils.setuptools_build import SETUPTOOLS_SHIM
from notpip._internal.utils.temp_dir import TempDirectory
from notpip._internal.utils.ui import open_spinner
from notpip._internal.vcs import vcs
from notpip._internal.wheel import Wheel, move_wheel_files
logger = logging.getLogger(__name__)
operators = specifiers.Specifier._operators.keys()
def _strip_extras(path):
m = re.match(r'^(.+)(\[[^\]]+\])$', path)
extras = None
@@ -65,40 +63,42 @@ def _strip_extras(path):
return path_no_extras, extras
def _safe_extras(extras):
return set(pkg_resources.safe_extra(extra) for extra in extras)
class InstallRequirement(object):
"""
Represents something that may be installed later on, may have information
about where to fetch the relavant requirement and also contains logic for
installing the said requirement.
"""
def __init__(self, req, comes_from, source_dir=None, editable=False,
link=None, as_egg=False, update=True,
pycompile=True, markers=None, isolated=False, options=None,
wheel_cache=None, constraint=False):
self.extras = ()
if isinstance(req, six.string_types):
try:
req = Requirement(req)
except InvalidRequirement:
if os.path.sep in req:
add_msg = "It looks like a path. Does it exist ?"
elif '=' in req and not any(op in req for op in operators):
add_msg = "= is not a valid operator. Did you mean == ?"
else:
add_msg = traceback.format_exc()
raise InstallationError(
"Invalid requirement: '%s'\n%s" % (req, add_msg))
self.extras = _safe_extras(req.extras)
link=None, update=True, markers=None,
isolated=False, options=None, wheel_cache=None,
constraint=False, extras=()):
assert req is None or isinstance(req, Requirement), req
self.req = req
self.comes_from = comes_from
self.constraint = constraint
self.source_dir = source_dir
if source_dir is not None:
self.source_dir = os.path.normpath(os.path.abspath(source_dir))
else:
self.source_dir = None
self.editable = editable
self._wheel_cache = wheel_cache
self.link = self.original_link = link
self.as_egg = as_egg
if link is not None:
self.link = self.original_link = link
else:
from notpip._internal.index import Link
self.link = self.original_link = req and req.url and Link(req.url)
if extras:
self.extras = extras
elif req:
self.extras = {
pkg_resources.safe_extra(extra) for extra in req.extras
}
else:
self.extras = set()
if markers is not None:
self.markers = markers
else:
@@ -111,7 +111,7 @@ class InstallRequirement(object):
# conflicts with another installed distribution:
self.conflicts_with = None
# Temporary build location
self._temp_build_dir = None
self._temp_build_dir = TempDirectory(kind="req-build")
# Used to store the global directory where the _temp_build_dir should
# have been created. Cf _correct_build_location method.
self._ideal_build_dir = None
@@ -120,44 +120,56 @@ class InstallRequirement(object):
# Set to True after successful installation
self.install_succeeded = None
# UninstallPathSet of uninstalled distribution (for possible rollback)
self.uninstalled = None
# Set True if a legitimate do-nothing-on-uninstall has happened - e.g.
# system site packages, stdlib packages.
self.nothing_to_uninstall = False
self.use_user_site = False
self.target_dir = None
self.uninstalled_pathset = None
self.options = options if options else {}
self.pycompile = pycompile
# Set to True after successful preparation of this requirement
self.prepared = False
self.is_direct = False
self.isolated = isolated
self.build_env = BuildEnvironment(no_clean=True)
@classmethod
def from_editable(cls, editable_req, comes_from=None, default_vcs=None,
isolated=False, options=None, wheel_cache=None,
constraint=False):
from pip9.index import Link
def from_editable(cls, editable_req, comes_from=None, isolated=False,
options=None, wheel_cache=None, constraint=False):
from notpip._internal.index import Link
name, url, extras_override = parse_editable(
editable_req, default_vcs)
name, url, extras_override = parse_editable(editable_req)
if url.startswith('file:'):
source_dir = url_to_path(url)
else:
source_dir = None
res = cls(name, comes_from, source_dir=source_dir,
editable=True,
link=Link(url),
constraint=constraint,
isolated=isolated,
options=options if options else {},
wheel_cache=wheel_cache)
if name is not None:
try:
req = Requirement(name)
except InvalidRequirement:
raise InstallationError("Invalid requirement: '%s'" % name)
else:
req = None
return cls(
req, comes_from, source_dir=source_dir,
editable=True,
link=Link(url),
constraint=constraint,
isolated=isolated,
options=options if options else {},
wheel_cache=wheel_cache,
extras=extras_override or (),
)
if extras_override is not None:
res.extras = _safe_extras(extras_override)
return res
@classmethod
def from_req(cls, req, comes_from=None, isolated=False, wheel_cache=None):
try:
req = Requirement(req)
except InvalidRequirement:
raise InstallationError("Invalid requirement: '%s'" % req)
if req.url:
raise InstallationError(
"Direct url requirement (like %s) are not allowed for "
"dependencies" % req
)
return cls(req, comes_from, isolated=isolated, wheel_cache=wheel_cache)
@classmethod
def from_line(
@@ -166,7 +178,7 @@ class InstallRequirement(object):
"""Creates an InstallRequirement from a name, which might be a
requirement, directory containing 'setup.py', filename, or URL.
"""
from pip9.index import Link
from notpip._internal.index import Link
if is_url(name):
marker_sep = '; '
@@ -191,9 +203,12 @@ class InstallRequirement(object):
link = Link(name)
else:
p, extras = _strip_extras(path)
if (os.path.isdir(p) and
(os.path.sep in name or name.startswith('.'))):
looks_like_dir = os.path.isdir(p) and (
os.path.sep in name or
(os.path.altsep is not None and os.path.altsep in name) or
name.startswith('.')
)
if looks_like_dir:
if not is_installable_dir(p):
raise InstallationError(
"Directory %r is not installable. File 'setup.py' "
@@ -228,16 +243,31 @@ class InstallRequirement(object):
else:
req = name
options = options if options else {}
res = cls(req, comes_from, link=link, markers=markers,
isolated=isolated, options=options,
wheel_cache=wheel_cache, constraint=constraint)
if extras:
res.extras = _safe_extras(
Requirement('placeholder' + extras).extras)
return res
extras = Requirement("placeholder" + extras.lower()).extras
else:
extras = ()
if req is not None:
try:
req = Requirement(req)
except InvalidRequirement:
if os.path.sep in req:
add_msg = "It looks like a path."
add_msg += deduce_helpful_msg(req)
elif '=' in req and not any(op in req for op in operators):
add_msg = "= is not a valid operator. Did you mean == ?"
else:
add_msg = traceback.format_exc()
raise InstallationError(
"Invalid requirement: '%s'\n%s" % (req, add_msg))
return cls(
req, comes_from, link=link, markers=markers,
isolated=isolated,
options=options if options else {},
wheel_cache=wheel_cache,
constraint=constraint,
extras=extras,
)
def __str__(self):
if self.req:
@@ -277,7 +307,7 @@ class InstallRequirement(object):
self.link = finder.find_requirement(self, upgrade)
if self._wheel_cache is not None and not require_hashes:
old_link = self.link
self.link = self._wheel_cache.cached_wheel(self.link, self.name)
self.link = self._wheel_cache.get(self.link, self.name)
if old_link != self.link:
logger.debug('Using cached wheel link: %s', self.link)
@@ -293,7 +323,7 @@ class InstallRequirement(object):
"""
specifiers = self.specifier
return (len(specifiers) == 1 and
next(iter(specifiers)).operator in ('==', '==='))
next(iter(specifiers)).operator in {'==', '==='})
def from_path(self):
if self.req is None:
@@ -309,8 +339,9 @@ class InstallRequirement(object):
return s
def build_location(self, build_dir):
if self._temp_build_dir is not None:
return self._temp_build_dir
assert build_dir is not None
if self._temp_build_dir.path is not None:
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
@@ -319,11 +350,10 @@ class InstallRequirement(object):
# 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 = os.path.realpath(
tempfile.mkdtemp('-build', 'pip-')
)
self._temp_build_dir.create()
self._ideal_build_dir = build_dir
return self._temp_build_dir
return self._temp_build_dir.path
if self.editable:
name = self.name.lower()
else:
@@ -348,10 +378,11 @@ class InstallRequirement(object):
if self.source_dir is not None:
return
assert self.req is not None
assert self._temp_build_dir
assert self._ideal_build_dir
old_location = self._temp_build_dir
self._temp_build_dir = None
assert self._temp_build_dir.path
assert self._ideal_build_dir.path
old_location = self._temp_build_dir.path
self._temp_build_dir.path = None
new_location = self.build_location(self._ideal_build_dir)
if os.path.exists(new_location):
raise InstallationError(
@@ -362,9 +393,9 @@ class InstallRequirement(object):
self, display_path(old_location), display_path(new_location),
)
shutil.move(old_location, new_location)
self._temp_build_dir = new_location
self._temp_build_dir.path = new_location
self._ideal_build_dir = None
self.source_dir = new_location
self.source_dir = os.path.normpath(os.path.abspath(new_location))
self._egg_info_path = None
@property
@@ -382,18 +413,6 @@ class InstallRequirement(object):
@property
def setup_py(self):
assert self.source_dir, "No source dir for %s" % self
try:
import setuptools # noqa
except ImportError:
if get_installed_version('setuptools') is None:
add_msg = "Please install setuptools."
else:
add_msg = traceback.format_exc()
# Setuptools is not available
raise InstallationError(
"Could not import setuptools which is required to "
"install from a source distribution.\n%s" % add_msg
)
setup_py = os.path.join(self.setup_py_dir, 'setup.py')
@@ -403,6 +422,34 @@ class InstallRequirement(object):
return setup_py
@property
def pyproject_toml(self):
assert self.source_dir, "No source dir for %s" % self
pp_toml = os.path.join(self.setup_py_dir, 'pyproject.toml')
# Python2 __file__ should not be unicode
if six.PY2 and isinstance(pp_toml, six.text_type):
pp_toml = pp_toml.encode(sys.getfilesystemencoding())
return pp_toml
def get_pep_518_info(self):
"""Get a list of the packages required to build the project, if any,
and a flag indicating whether pyproject.toml is present, indicating
that the build should be isolated.
Build requirements can be specified in a pyproject.toml, as described
in PEP 518. If this file exists but doesn't specify build
requirements, pip will default to installing setuptools and wheel.
"""
if os.path.isfile(self.pyproject_toml):
with open(self.pyproject_toml) as f:
pp_toml = pytoml.load(f)
build_sys = pp_toml.get('build-system', {})
return (build_sys.get('requires', ['setuptools', 'wheel']), True)
return (['setuptools', 'wheel'], False)
def run_egg_info(self):
assert self.source_dir
if self.name:
@@ -418,7 +465,7 @@ class InstallRequirement(object):
with indent_log():
script = SETUPTOOLS_SHIM % self.setup_py
base_cmd = [os.environ['PIP_PYTHON_PATH'], '-c', script]
base_cmd = [os.environ.get('PIP_PYTHON_PATH', sys.executable), '-c', script]
if self.isolated:
base_cmd += ["--no-user-cfg"]
egg_info_cmd = base_cmd + ['egg_info']
@@ -431,11 +478,12 @@ class InstallRequirement(object):
egg_info_dir = os.path.join(self.setup_py_dir, 'pip-egg-info')
ensure_dir(egg_info_dir)
egg_base_option = ['--egg-base', 'pip-egg-info']
call_subprocess(
egg_info_cmd + egg_base_option,
cwd=self.setup_py_dir,
show_stdout=False,
command_desc='python setup.py egg_info')
with self.build_env:
call_subprocess(
egg_info_cmd + egg_base_option,
cwd=self.setup_py_dir,
show_stdout=False,
command_desc='python setup.py egg_info')
if not self.req:
if isinstance(parse_version(self.pkg_info()["Version"]), Version):
@@ -506,7 +554,7 @@ class InstallRequirement(object):
elif dir == 'test' or dir == 'tests':
dirs.remove(dir)
filenames.extend([os.path.join(root, dir)
for dir in dirs])
for dir in dirs])
filenames = [f for f in filenames if f.endswith('.egg-info')]
if not filenames:
@@ -551,7 +599,7 @@ class InstallRequirement(object):
logger.warning(
'Requested %s, but installing version %s',
self,
self.installed_version,
version,
)
else:
logger.debug(
@@ -590,7 +638,8 @@ class InstallRequirement(object):
'Unexpected version control type (in %s): %s'
% (self.link, vc_type))
def uninstall(self, auto_confirm=False):
def uninstall(self, auto_confirm=False, verbose=False,
use_user_site=False):
"""
Uninstall the distribution currently satisfying this requirement.
@@ -603,171 +652,14 @@ class InstallRequirement(object):
linked to global site-packages.
"""
if not self.check_if_exists():
raise UninstallationError(
"Cannot uninstall requirement %s, not installed" % (self.name,)
)
if not self.check_if_exists(use_user_site):
logger.warning("Skipping %s as it is not installed.", self.name)
return
dist = self.satisfied_by or self.conflicts_with
dist_path = normalize_path(dist.location)
if not dist_is_local(dist):
logger.info(
"Not uninstalling %s at %s, outside environment %s",
dist.key,
dist_path,
sys.prefix,
)
self.nothing_to_uninstall = True
return
if dist_path in get_stdlib():
logger.info(
"Not uninstalling %s at %s, as it is in the standard library.",
dist.key,
dist_path,
)
self.nothing_to_uninstall = True
return
paths_to_remove = UninstallPathSet(dist)
develop_egg_link = egg_link_path(dist)
develop_egg_link_egg_info = '{0}.egg-info'.format(
pkg_resources.to_filename(dist.project_name))
egg_info_exists = dist.egg_info and os.path.exists(dist.egg_info)
# Special case for distutils installed package
distutils_egg_info = getattr(dist._provider, 'path', None)
# Uninstall cases order do matter as in the case of 2 installs of the
# same package, pip needs to uninstall the currently detected version
if (egg_info_exists and dist.egg_info.endswith('.egg-info') and
not dist.egg_info.endswith(develop_egg_link_egg_info)):
# if dist.egg_info.endswith(develop_egg_link_egg_info), we
# are in fact in the develop_egg_link case
paths_to_remove.add(dist.egg_info)
if dist.has_metadata('installed-files.txt'):
for installed_file in dist.get_metadata(
'installed-files.txt').splitlines():
path = os.path.normpath(
os.path.join(dist.egg_info, installed_file)
)
paths_to_remove.add(path)
# FIXME: need a test for this elif block
# occurs with --single-version-externally-managed/--record outside
# of pip
elif dist.has_metadata('top_level.txt'):
if dist.has_metadata('namespace_packages.txt'):
namespaces = dist.get_metadata('namespace_packages.txt')
else:
namespaces = []
for top_level_pkg in [
p for p
in dist.get_metadata('top_level.txt').splitlines()
if p and p not in namespaces]:
path = os.path.join(dist.location, top_level_pkg)
paths_to_remove.add(path)
paths_to_remove.add(path + '.py')
paths_to_remove.add(path + '.pyc')
paths_to_remove.add(path + '.pyo')
elif distutils_egg_info:
warnings.warn(
"Uninstalling a distutils installed project ({0}) has been "
"deprecated and will be removed in a future version. This is "
"due to the fact that uninstalling a distutils project will "
"only partially uninstall the project.".format(self.name),
RemovedInPip10Warning,
)
paths_to_remove.add(distutils_egg_info)
elif dist.location.endswith('.egg'):
# package installed by easy_install
# We cannot match on dist.egg_name because it can slightly vary
# i.e. setuptools-0.6c11-py2.6.egg vs setuptools-0.6rc11-py2.6.egg
paths_to_remove.add(dist.location)
easy_install_egg = os.path.split(dist.location)[1]
easy_install_pth = os.path.join(os.path.dirname(dist.location),
'easy-install.pth')
paths_to_remove.add_pth(easy_install_pth, './' + easy_install_egg)
elif egg_info_exists and dist.egg_info.endswith('.dist-info'):
for path in pip9.wheel.uninstallation_paths(dist):
paths_to_remove.add(path)
elif develop_egg_link:
# develop egg
with open(develop_egg_link, 'r') as fh:
link_pointer = os.path.normcase(fh.readline().strip())
assert (link_pointer == dist.location), (
'Egg-link %s does not match installed location of %s '
'(at %s)' % (link_pointer, self.name, dist.location)
)
paths_to_remove.add(develop_egg_link)
easy_install_pth = os.path.join(os.path.dirname(develop_egg_link),
'easy-install.pth')
paths_to_remove.add_pth(easy_install_pth, dist.location)
else:
logger.debug(
'Not sure how to uninstall: %s - Check: %s',
dist, dist.location)
# find distutils scripts= scripts
if dist.has_metadata('scripts') and dist.metadata_isdir('scripts'):
for script in dist.metadata_listdir('scripts'):
if dist_in_usersite(dist):
bin_dir = bin_user
else:
bin_dir = bin_py
paths_to_remove.add(os.path.join(bin_dir, script))
if WINDOWS:
paths_to_remove.add(os.path.join(bin_dir, script) + '.bat')
# find console_scripts
if dist.has_metadata('entry_points.txt'):
if six.PY2:
options = {}
else:
options = {"delimiters": ('=', )}
config = configparser.SafeConfigParser(**options)
config.readfp(
FakeFile(dist.get_metadata_lines('entry_points.txt'))
)
if config.has_section('console_scripts'):
for name, value in config.items('console_scripts'):
if dist_in_usersite(dist):
bin_dir = bin_user
else:
bin_dir = bin_py
paths_to_remove.add(os.path.join(bin_dir, name))
if WINDOWS:
paths_to_remove.add(
os.path.join(bin_dir, name) + '.exe'
)
paths_to_remove.add(
os.path.join(bin_dir, name) + '.exe.manifest'
)
paths_to_remove.add(
os.path.join(bin_dir, name) + '-script.py'
)
paths_to_remove.remove(auto_confirm)
self.uninstalled = paths_to_remove
def rollback_uninstall(self):
if self.uninstalled:
self.uninstalled.rollback()
else:
logger.error(
"Can't rollback %s, nothing uninstalled.", self.name,
)
def commit_uninstall(self):
if self.uninstalled:
self.uninstalled.commit()
elif not self.nothing_to_uninstall:
logger.error(
"Can't commit %s, nothing uninstalled.", self.name,
)
uninstalled_pathset = UninstallPathSet.from_dist(dist)
uninstalled_pathset.remove(auto_confirm, verbose)
return uninstalled_pathset
def archive(self, build_dir):
assert self.source_dir
@@ -837,17 +729,24 @@ class InstallRequirement(object):
else:
return True
def install(self, install_options, global_options=[], root=None,
prefix=None):
def install(self, install_options, global_options=None, root=None,
home=None, prefix=None, warn_script_location=True,
use_user_site=False, pycompile=True):
global_options = global_options if global_options is not None else []
if self.editable:
self.install_editable(
install_options, global_options, prefix=prefix)
install_options, global_options, prefix=prefix,
)
return
if self.is_wheel:
version = pip9.wheel.wheel_version(self.source_dir)
pip9.wheel.check_compatibility(version, self.name)
version = wheel.wheel_version(self.source_dir)
wheel.check_compatibility(version, self.name)
self.move_wheel_files(self.source_dir, root=root, prefix=prefix)
self.move_wheel_files(
self.source_dir, root=root, prefix=prefix, home=home,
warn_script_location=warn_script_location,
use_user_site=use_user_site, pycompile=pycompile,
)
self.install_succeeded = True
return
@@ -856,35 +755,34 @@ class InstallRequirement(object):
# 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 += self.options.get('global_options', [])
install_options += self.options.get('install_options', [])
global_options = list(global_options) + \
self.options.get('global_options', [])
install_options = list(install_options) + \
self.options.get('install_options', [])
if self.isolated:
global_options = list(global_options) + ["--no-user-cfg"]
global_options = global_options + ["--no-user-cfg"]
temp_location = tempfile.mkdtemp('-record', 'pip-')
record_filename = os.path.join(temp_location, 'install-record.txt')
try:
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)
global_options, record_filename, root, prefix, pycompile,
)
msg = 'Running setup.py install for %s' % (self.name,)
with open_spinner(msg) as spinner:
with indent_log():
call_subprocess(
install_args + install_options,
cwd=self.setup_py_dir,
show_stdout=False,
spinner=spinner,
)
with self.build_env:
call_subprocess(
install_args + install_options,
cwd=self.setup_py_dir,
show_stdout=False,
spinner=spinner,
)
if not os.path.exists(record_filename):
logger.debug('Record file %s not found', record_filename)
return
self.install_succeeded = True
if self.as_egg:
# there's no --always-unzip option we can pass to install
# command so we unable to save the installed-files.txt
return
def prepend_root(path):
if root is None or not os.path.isabs(path):
@@ -914,16 +812,13 @@ class InstallRequirement(object):
if os.path.isdir(filename):
filename += os.path.sep
new_lines.append(
os.path.relpath(
prepend_root(filename), egg_info_dir)
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')
finally:
if os.path.exists(record_filename):
os.remove(record_filename)
rmtree(temp_location)
def ensure_has_source_dir(self, parent_dir):
"""Ensure that a source_dir is set.
@@ -939,22 +834,21 @@ class InstallRequirement(object):
self.source_dir = self.build_location(parent_dir)
return self.source_dir
def get_install_args(self, global_options, record_filename, root, prefix):
install_args = [os.environ['PIP_PYTHON_PATH'], "-u"]
def get_install_args(self, global_options, record_filename, root, prefix,
pycompile):
install_args = [os.environ.get('PIP_PYTHON_PATH', sys.executable), "-u"]
install_args.append('-c')
install_args.append(SETUPTOOLS_SHIM % self.setup_py)
install_args += list(global_options) + \
['install', '--record', record_filename]
if not self.as_egg:
install_args += ['--single-version-externally-managed']
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 self.pycompile:
if pycompile:
install_args += ["--compile"]
else:
install_args += ["--no-compile"]
@@ -975,9 +869,8 @@ class InstallRequirement(object):
logger.debug('Removing source in %s', self.source_dir)
rmtree(self.source_dir)
self.source_dir = None
if self._temp_build_dir and os.path.exists(self._temp_build_dir):
rmtree(self._temp_build_dir)
self._temp_build_dir = None
self._temp_build_dir.cleanup()
self.build_env.cleanup()
def install_editable(self, install_options,
global_options=(), prefix=None):
@@ -987,27 +880,29 @@ class InstallRequirement(object):
global_options = list(global_options) + ["--no-user-cfg"]
if prefix:
prefix_param = ['--prefix={0}'.format(prefix)]
prefix_param = ['--prefix={}'.format(prefix)]
install_options = list(install_options) + prefix_param
with indent_log():
# FIXME: should we do --install-headers here too?
call_subprocess(
[
os.environ['PIP_PYTHON_PATH'],
'-c',
SETUPTOOLS_SHIM % self.setup_py
] +
list(global_options) +
['develop', '--no-deps'] +
list(install_options),
with self.build_env:
call_subprocess(
[
os.environ.get('PIP_PYTHON_PATH', sys.executable),
'-c',
SETUPTOOLS_SHIM % self.setup_py
] +
list(global_options) +
['develop', '--no-deps'] +
list(install_options),
cwd=self.setup_py_dir,
show_stdout=False)
cwd=self.setup_py_dir,
show_stdout=False,
)
self.install_succeeded = True
def check_if_exists(self):
def check_if_exists(self, use_user_site):
"""Find an installed distribution that satisfies or conflicts
with this requirement, and set self.satisfied_by or
self.conflicts_with appropriately.
@@ -1034,7 +929,7 @@ class InstallRequirement(object):
existing_dist = pkg_resources.get_distribution(
self.req.name
)
if self.use_user_site:
if use_user_site:
if dist_in_usersite(existing_dist):
self.conflicts_with = existing_dist
elif (running_under_virtualenv() and
@@ -1052,27 +947,31 @@ class InstallRequirement(object):
def is_wheel(self):
return self.link and self.link.is_wheel
def move_wheel_files(self, wheeldir, root=None, prefix=None):
def move_wheel_files(self, wheeldir, root=None, home=None, prefix=None,
warn_script_location=True, use_user_site=False,
pycompile=True):
move_wheel_files(
self.name, self.req, wheeldir,
user=self.use_user_site,
home=self.target_dir,
user=use_user_site,
home=home,
root=root,
prefix=prefix,
pycompile=self.pycompile,
pycompile=pycompile,
isolated=self.isolated,
warn_script_location=warn_script_location,
)
def get_dist(self):
"""Return a pkg_resources.Distribution built from self.egg_info_path"""
egg_info = self.egg_info_path('').rstrip('/')
egg_info = self.egg_info_path('').rstrip(os.path.sep)
base_dir = os.path.dirname(egg_info)
metadata = pkg_resources.PathMetadata(base_dir, egg_info)
dist_name = os.path.splitext(os.path.basename(egg_info))[0]
return pkg_resources.Distribution(
os.path.dirname(egg_info),
project_name=dist_name,
metadata=metadata)
metadata=metadata,
)
@property
def has_hash_options(self):
@@ -1114,11 +1013,15 @@ def _strip_postfix(req):
match = re.search(r'^(.*?)(?:-dev|-\d.*)$', req)
if match:
# Strip off -dev, -0.2, etc.
warnings.warn(
"#egg cleanup for editable urls will be dropped in the future",
RemovedInPip11Warning,
)
req = match.group(1)
return req
def parse_editable(editable_req, default_vcs=None):
def parse_editable(editable_req):
"""Parses an editable requirement into:
- a requirement name
- an URL
@@ -1129,18 +1032,12 @@ def parse_editable(editable_req, default_vcs=None):
.[some_extra]
"""
from pip9.index import Link
from notpip._internal.index import Link
url = editable_req
extras = None
# If a file path is specified with extras, strip off the extras.
m = re.match(r'^(.+)(\[[^\]]+\])$', url)
if m:
url_no_extras = m.group(1)
extras = m.group(2)
else:
url_no_extras = url
url_no_extras, extras = _strip_extras(url)
if os.path.isdir(url_no_extras):
if not os.path.exists(os.path.join(url_no_extras, 'setup.py')):
@@ -1168,19 +1065,11 @@ def parse_editable(editable_req, default_vcs=None):
break
if '+' not in url:
if default_vcs:
warnings.warn(
"--default-vcs has been deprecated and will be removed in "
"the future.",
RemovedInPip10Warning,
)
url = default_vcs + '+' + url
else:
raise InstallationError(
'%s should either be a path to a local project or a VCS url '
'beginning with svn+, git+, hg+, or bzr+' %
editable_req
)
raise InstallationError(
'%s should either be a path to a local project or a VCS url '
'beginning with svn+, git+, hg+, or bzr+' %
editable_req
)
vc_type = url.split('+', 1)[0].lower()
@@ -1193,11 +1082,34 @@ def parse_editable(editable_req, default_vcs=None):
package_name = Link(url).egg_fragment
if not package_name:
raise InstallationError(
"Could not detect requirement name, please specify one with #egg="
)
if not package_name:
raise InstallationError(
'--editable=%s is not the right format; it must have '
'#egg=Package' % editable_req
"Could not detect requirement name for '%s', please specify one "
"with #egg=your_package_name" % editable_req
)
return _strip_postfix(package_name), url, None
def deduce_helpful_msg(req):
"""Returns helpful msg in case requirements file does not exist,
or cannot be parsed.
:params req: Requirements file path
"""
msg = ""
if os.path.exists(req):
msg = " It does exist."
# Try to parse and check if it is a requirements file.
try:
with open(req, 'r') as fp:
# parse first line only
next(parse_requirements(fp.read()))
msg += " The argument you provided " + \
"(%s) appears to be a" % (req) + \
" requirements file. If that is the" + \
" case, use the '-r' flag to install" + \
" the packages specified within it."
except RequirementParseError:
logger.debug("Cannot parse '%s' as requirements \
file" % (req), exc_info=1)
else:
msg += " File '%s' does not exist." % (req)
return msg
@@ -0,0 +1,165 @@
from __future__ import absolute_import
import logging
from collections import OrderedDict
from notpip._internal.exceptions import InstallationError
from notpip._internal.utils.logging import indent_log
from notpip._internal.wheel import Wheel
logger = logging.getLogger(__name__)
class RequirementSet(object):
def __init__(self, require_hashes=False, ignore_compatibility=True):
"""Create a RequirementSet.
:param wheel_cache: The pip wheel cache, for passing to
InstallRequirement.
"""
self.requirements = OrderedDict()
self.require_hashes = require_hashes
# Mapping of alias: real_name
self.requirement_aliases = {}
self.unnamed_requirements = []
self.successfully_downloaded = []
self.reqs_to_cleanup = []
self.ignore_compatibility = ignore_compatibility
def __str__(self):
reqs = [req for req in self.requirements.values()
if not req.comes_from]
reqs.sort(key=lambda req: req.name.lower())
return ' '.join([str(req.req) for req in reqs])
def __repr__(self):
reqs = [req for req in self.requirements.values()]
reqs.sort(key=lambda req: req.name.lower())
reqs_str = ', '.join([str(req.req) for req in reqs])
return ('<%s object; %d requirement(s): %s>'
% (self.__class__.__name__, len(reqs), reqs_str))
def add_requirement(self, install_req, parent_req_name=None,
extras_requested=None):
"""Add install_req as a requirement to install.
:param parent_req_name: The name of the requirement that needed this
added. The name is used because when multiple unnamed requirements
resolve to the same name, we could otherwise end up with dependency
links that point outside the Requirements set. parent_req must
already be added. Note that None implies that this is a user
supplied requirement, vs an inferred one.
:param extras_requested: an iterable of extras used to evaluate the
environment markers.
:return: Additional requirements to scan. That is either [] if
the requirement is not applicable, or [install_req] if the
requirement is applicable and has just been added.
"""
name = install_req.name
if not install_req.match_markers(extras_requested):
logger.info("Ignoring %s: markers '%s' don't match your "
"environment", install_req.name,
install_req.markers)
return [], None
# This check has to come after we filter requirements with the
# environment markers.
if install_req.link and install_req.link.is_wheel:
wheel = Wheel(install_req.link.filename)
if not wheel.supported() and not self.ignore_compatibility:
raise InstallationError(
"%s is not a supported wheel on this platform." %
wheel.filename
)
# This next bit is really a sanity check.
assert install_req.is_direct == (parent_req_name is None), (
"a direct req shouldn't have a parent and also, "
"a non direct req should have a parent"
)
if not name:
# url or path requirement w/o an egg fragment
self.unnamed_requirements.append(install_req)
return [install_req], None
else:
try:
existing_req = self.get_requirement(name)
except KeyError:
existing_req = None
if (parent_req_name is None and existing_req and not
existing_req.constraint and
existing_req.extras == install_req.extras and not
existing_req.req.specifier == install_req.req.specifier):
raise InstallationError(
'Double requirement given: %s (already in %s, name=%r)'
% (install_req, existing_req, name))
if not existing_req:
# Add requirement
self.requirements[name] = install_req
# FIXME: what about other normalizations? E.g., _ vs. -?
if name.lower() != name:
self.requirement_aliases[name.lower()] = name
result = [install_req]
else:
# Assume there's no need to scan, and that we've already
# encountered this for scanning.
result = []
if not install_req.constraint and existing_req.constraint:
if (install_req.link and not (existing_req.link and
install_req.link.path == existing_req.link.path)):
self.reqs_to_cleanup.append(install_req)
raise InstallationError(
"Could not satisfy constraints for '%s': "
"installation from path or url cannot be "
"constrained to a version" % name,
)
# If we're now installing a constraint, mark the existing
# object for real installation.
existing_req.constraint = False
existing_req.extras = tuple(
sorted(set(existing_req.extras).union(
set(install_req.extras))))
logger.debug("Setting %s extras to: %s",
existing_req, existing_req.extras)
# And now we need to scan this.
result = [existing_req]
# Canonicalise to the already-added object for the backref
# check below.
install_req = existing_req
# We return install_req here to allow for the caller to add it to
# the dependency information for the parent package.
return result, install_req
def has_requirement(self, project_name):
name = project_name.lower()
if (name in self.requirements and
not self.requirements[name].constraint or
name in self.requirement_aliases and
not self.requirements[self.requirement_aliases[name]].constraint):
return True
return False
@property
def has_requirements(self):
return list(req for req in self.requirements.values() if not
req.constraint) or self.unnamed_requirements
def get_requirement(self, project_name):
for name in project_name, project_name.lower():
if name in self.requirements:
return self.requirements[name]
if name in self.requirement_aliases:
return self.requirements[self.requirement_aliases[name]]
# raise KeyError("No project with the name %r" % project_name)
def cleanup_files(self):
"""Clean up files, remove builds."""
logger.debug('Cleaning up...')
with indent_log():
for req in self.reqs_to_cleanup:
req.remove_temporary_source()
@@ -0,0 +1,455 @@
from __future__ import absolute_import
import csv
import functools
import logging
import os
import sys
import sysconfig
from notpip._vendor import pkg_resources
from notpip._internal.compat import WINDOWS, cache_from_source, uses_pycache
from notpip._internal.exceptions import UninstallationError
from notpip._internal.locations import bin_py, bin_user
from notpip._internal.utils.logging import indent_log
from notpip._internal.utils.misc import (
FakeFile, ask, dist_in_usersite, dist_is_local, egg_link_path, is_local,
normalize_path, renames,
)
from notpip._internal.utils.temp_dir import TempDirectory
logger = logging.getLogger(__name__)
def _script_names(dist, script_name, is_gui):
"""Create the fully qualified name of the files created by
{console,gui}_scripts for the given ``dist``.
Returns the list of file names
"""
if dist_in_usersite(dist):
bin_dir = bin_user
else:
bin_dir = bin_py
exe_name = os.path.join(bin_dir, script_name)
paths_to_remove = [exe_name]
if WINDOWS:
paths_to_remove.append(exe_name + '.exe')
paths_to_remove.append(exe_name + '.exe.manifest')
if is_gui:
paths_to_remove.append(exe_name + '-script.pyw')
else:
paths_to_remove.append(exe_name + '-script.py')
return paths_to_remove
def _unique(fn):
@functools.wraps(fn)
def unique(*args, **kw):
seen = set()
for item in fn(*args, **kw):
if item not in seen:
seen.add(item)
yield item
return unique
@_unique
def uninstallation_paths(dist):
"""
Yield all the uninstallation paths for dist based on RECORD-without-.pyc
Yield paths to all the files in RECORD. For each .py file in RECORD, add
the .pyc in the same directory.
UninstallPathSet.add() takes care of the __pycache__ .pyc.
"""
r = csv.reader(FakeFile(dist.get_metadata_lines('RECORD')))
for row in r:
path = os.path.join(dist.location, row[0])
yield path
if path.endswith('.py'):
dn, fn = os.path.split(path)
base = fn[:-3]
path = os.path.join(dn, base + '.pyc')
yield path
def compact(paths):
"""Compact a path set to contain the minimal number of paths
necessary to contain all paths in the set. If /a/path/ and
/a/path/to/a/file.txt are both in the set, leave only the
shorter path."""
sep = os.path.sep
short_paths = set()
for path in sorted(paths, key=len):
should_add = any(
path.startswith(shortpath.rstrip("*")) and
path[len(shortpath.rstrip("*").rstrip(sep))] == sep
for shortpath in short_paths
)
if not should_add:
short_paths.add(path)
return short_paths
def compress_for_output_listing(paths):
"""Returns a tuple of 2 sets of which paths to display to user
The first set contains paths that would be deleted. Files of a package
are not added and the top-level directory of the package has a '*' added
at the end - to signify that all it's contents are removed.
The second set contains files that would have been skipped in the above
folders.
"""
will_remove = list(paths)
will_skip = set()
# Determine folders and files
folders = set()
files = set()
for path in will_remove:
if path.endswith(".pyc"):
continue
if path.endswith("__init__.py") or ".dist-info" in path:
folders.add(os.path.dirname(path))
files.add(path)
folders = compact(folders)
# This walks the tree using os.walk to not miss extra folders
# that might get added.
for folder in folders:
for dirpath, _, dirfiles in os.walk(folder):
for fname in dirfiles:
if fname.endswith(".pyc"):
continue
file_ = os.path.normcase(os.path.join(dirpath, fname))
if os.path.isfile(file_) and file_ not in files:
# We are skipping this file. Add it to the set.
will_skip.add(file_)
will_remove = files | {
os.path.join(folder, "*") for folder in folders
}
return will_remove, will_skip
class UninstallPathSet(object):
"""A set of file paths to be removed in the uninstallation of a
requirement."""
def __init__(self, dist):
self.paths = set()
self._refuse = set()
self.pth = {}
self.dist = dist
self.save_dir = TempDirectory(kind="uninstall")
self._moved_paths = []
def _permitted(self, path):
"""
Return True if the given path is one we are permitted to
remove/modify, False otherwise.
"""
return is_local(path)
def add(self, path):
head, tail = os.path.split(path)
# we normalize the head to resolve parent directory symlinks, but not
# the tail, since we only want to uninstall symlinks, not their targets
path = os.path.join(normalize_path(head), os.path.normcase(tail))
if not os.path.exists(path):
return
if self._permitted(path):
self.paths.add(path)
else:
self._refuse.add(path)
# __pycache__ files can show up after 'installed-files.txt' is created,
# due to imports
if os.path.splitext(path)[1] == '.py' and uses_pycache:
self.add(cache_from_source(path))
def add_pth(self, pth_file, entry):
pth_file = normalize_path(pth_file)
if self._permitted(pth_file):
if pth_file not in self.pth:
self.pth[pth_file] = UninstallPthEntries(pth_file)
self.pth[pth_file].add(entry)
else:
self._refuse.add(pth_file)
def _stash(self, path):
return os.path.join(
self.save_dir.path, os.path.splitdrive(path)[1].lstrip(os.path.sep)
)
def remove(self, auto_confirm=False, verbose=False):
"""Remove paths in ``self.paths`` with confirmation (unless
``auto_confirm`` is True)."""
if not self.paths:
logger.info(
"Can't uninstall '%s'. No files were found to uninstall.",
self.dist.project_name,
)
return
dist_name_version = (
self.dist.project_name + "-" + self.dist.version
)
logger.info('Uninstalling %s:', dist_name_version)
with indent_log():
if auto_confirm or self._allowed_to_proceed(verbose):
self.save_dir.create()
for path in sorted(compact(self.paths)):
new_path = self._stash(path)
logger.debug('Removing file or directory %s', path)
self._moved_paths.append(path)
renames(path, new_path)
for pth in self.pth.values():
pth.remove()
logger.info('Successfully uninstalled %s', dist_name_version)
def _allowed_to_proceed(self, verbose):
"""Display which files would be deleted and prompt for confirmation
"""
def _display(msg, paths):
if not paths:
return
logger.info(msg)
with indent_log():
for path in sorted(compact(paths)):
logger.info(path)
if not verbose:
will_remove, will_skip = compress_for_output_listing(self.paths)
else:
# In verbose mode, display all the files that are going to be
# deleted.
will_remove = list(self.paths)
will_skip = set()
_display('Would remove:', will_remove)
_display('Would not remove (might be manually added):', will_skip)
_display('Would not remove (outside of prefix):', self._refuse)
return ask('Proceed (y/n)? ', ('y', 'n')) == 'y'
def rollback(self):
"""Rollback the changes previously made by remove()."""
if self.save_dir.path is None:
logger.error(
"Can't roll back %s; was not uninstalled",
self.dist.project_name,
)
return False
logger.info('Rolling back uninstall of %s', self.dist.project_name)
for path in self._moved_paths:
tmp_path = self._stash(path)
logger.debug('Replacing %s', path)
renames(tmp_path, path)
for pth in self.pth.values():
pth.rollback()
def commit(self):
"""Remove temporary save dir: rollback will no longer be possible."""
self.save_dir.cleanup()
self._moved_paths = []
@classmethod
def from_dist(cls, dist):
dist_path = normalize_path(dist.location)
if not dist_is_local(dist):
logger.info(
"Not uninstalling %s at %s, outside environment %s",
dist.key,
dist_path,
sys.prefix,
)
return cls(dist)
if dist_path in {p for p in {sysconfig.get_path("stdlib"),
sysconfig.get_path("platstdlib")}
if p}:
logger.info(
"Not uninstalling %s at %s, as it is in the standard library.",
dist.key,
dist_path,
)
return cls(dist)
paths_to_remove = cls(dist)
develop_egg_link = egg_link_path(dist)
develop_egg_link_egg_info = '{}.egg-info'.format(
pkg_resources.to_filename(dist.project_name))
egg_info_exists = dist.egg_info and os.path.exists(dist.egg_info)
# Special case for distutils installed package
distutils_egg_info = getattr(dist._provider, 'path', None)
# Uninstall cases order do matter as in the case of 2 installs of the
# same package, pip needs to uninstall the currently detected version
if (egg_info_exists and dist.egg_info.endswith('.egg-info') and
not dist.egg_info.endswith(develop_egg_link_egg_info)):
# if dist.egg_info.endswith(develop_egg_link_egg_info), we
# are in fact in the develop_egg_link case
paths_to_remove.add(dist.egg_info)
if dist.has_metadata('installed-files.txt'):
for installed_file in dist.get_metadata(
'installed-files.txt').splitlines():
path = os.path.normpath(
os.path.join(dist.egg_info, installed_file)
)
paths_to_remove.add(path)
# FIXME: need a test for this elif block
# occurs with --single-version-externally-managed/--record outside
# of pip
elif dist.has_metadata('top_level.txt'):
if dist.has_metadata('namespace_packages.txt'):
namespaces = dist.get_metadata('namespace_packages.txt')
else:
namespaces = []
for top_level_pkg in [
p for p
in dist.get_metadata('top_level.txt').splitlines()
if p and p not in namespaces]:
path = os.path.join(dist.location, top_level_pkg)
paths_to_remove.add(path)
paths_to_remove.add(path + '.py')
paths_to_remove.add(path + '.pyc')
paths_to_remove.add(path + '.pyo')
elif distutils_egg_info:
raise UninstallationError(
"Cannot uninstall {!r}. It is a distutils installed project "
"and thus we cannot accurately determine which files belong "
"to it which would lead to only a partial uninstall.".format(
dist.project_name,
)
)
elif dist.location.endswith('.egg'):
# package installed by easy_install
# We cannot match on dist.egg_name because it can slightly vary
# i.e. setuptools-0.6c11-py2.6.egg vs setuptools-0.6rc11-py2.6.egg
paths_to_remove.add(dist.location)
easy_install_egg = os.path.split(dist.location)[1]
easy_install_pth = os.path.join(os.path.dirname(dist.location),
'easy-install.pth')
paths_to_remove.add_pth(easy_install_pth, './' + easy_install_egg)
elif egg_info_exists and dist.egg_info.endswith('.dist-info'):
for path in uninstallation_paths(dist):
paths_to_remove.add(path)
elif develop_egg_link:
# develop egg
with open(develop_egg_link, 'r') as fh:
link_pointer = os.path.normcase(fh.readline().strip())
assert (link_pointer == dist.location), (
'Egg-link %s does not match installed location of %s '
'(at %s)' % (link_pointer, dist.project_name, dist.location)
)
paths_to_remove.add(develop_egg_link)
easy_install_pth = os.path.join(os.path.dirname(develop_egg_link),
'easy-install.pth')
paths_to_remove.add_pth(easy_install_pth, dist.location)
else:
logger.debug(
'Not sure how to uninstall: %s - Check: %s',
dist, dist.location,
)
# find distutils scripts= scripts
if dist.has_metadata('scripts') and dist.metadata_isdir('scripts'):
for script in dist.metadata_listdir('scripts'):
if dist_in_usersite(dist):
bin_dir = bin_user
else:
bin_dir = bin_py
paths_to_remove.add(os.path.join(bin_dir, script))
if WINDOWS:
paths_to_remove.add(os.path.join(bin_dir, script) + '.bat')
# find console_scripts
_scripts_to_remove = []
console_scripts = dist.get_entry_map(group='console_scripts')
for name in console_scripts.keys():
_scripts_to_remove.extend(_script_names(dist, name, False))
# find gui_scripts
gui_scripts = dist.get_entry_map(group='gui_scripts')
for name in gui_scripts.keys():
_scripts_to_remove.extend(_script_names(dist, name, True))
for s in _scripts_to_remove:
paths_to_remove.add(s)
return paths_to_remove
class UninstallPthEntries(object):
def __init__(self, pth_file):
if not os.path.isfile(pth_file):
raise UninstallationError(
"Cannot remove entries from nonexistent file %s" % pth_file
)
self.file = pth_file
self.entries = set()
self._saved_lines = None
def add(self, entry):
entry = os.path.normcase(entry)
# On Windows, os.path.normcase converts the entry to use
# backslashes. This is correct for entries that describe absolute
# paths outside of site-packages, but all the others use forward
# slashes.
if WINDOWS and not os.path.splitdrive(entry)[0]:
entry = entry.replace('\\', '/')
self.entries.add(entry)
def remove(self):
logger.debug('Removing pth entries from %s:', self.file)
with open(self.file, 'rb') as fh:
# windows uses '\r\n' with py3k, but uses '\n' with py2.x
lines = fh.readlines()
self._saved_lines = lines
if any(b'\r\n' in line for line in lines):
endline = '\r\n'
else:
endline = '\n'
# handle missing trailing newline
if lines and not lines[-1].endswith(endline.encode("utf-8")):
lines[-1] = lines[-1] + endline.encode("utf-8")
for entry in self.entries:
try:
logger.debug('Removing entry: %s', entry)
lines.remove((entry + endline).encode("utf-8"))
except ValueError:
pass
with open(self.file, 'wb') as fh:
fh.writelines(lines)
def rollback(self):
if self._saved_lines is None:
logger.error(
'Cannot roll back changes to %s, none were made', self.file
)
return False
logger.debug('Rolling %s back to previous state', self.file)
with open(self.file, 'wb') as fh:
fh.writelines(self._saved_lines)
return True
+381
View File
@@ -0,0 +1,381 @@
"""Dependency Resolution
The dependency resolution in pip is performed as follows:
for top-level requirements:
a. only one spec allowed per project, regardless of conflicts or not.
otherwise a "double requirement" exception is raised
b. they override sub-dependency requirements.
for sub-dependencies
a. "first found, wins" (where the order is breadth first)
"""
import logging
from collections import defaultdict
from itertools import chain
from notpip._internal.exceptions import (
BestVersionAlreadyInstalled, DistributionNotFound, HashError, HashErrors,
UnsupportedPythonVersion,
)
from notpip._internal.req.req_install import InstallRequirement
from notpip._internal.utils.logging import indent_log
from notpip._internal.utils.misc import dist_in_usersite, ensure_dir
from notpip._internal.utils.packaging import check_dist_requires_python
logger = logging.getLogger(__name__)
class Resolver(object):
"""Resolves which packages need to be installed/uninstalled to perform \
the requested operation without breaking the requirements of any package.
"""
_allowed_strategies = {"eager", "only-if-needed", "to-satisfy-only"}
def __init__(self, preparer, session, finder, wheel_cache, use_user_site,
ignore_dependencies, ignore_installed, ignore_requires_python,
force_reinstall, isolated, upgrade_strategy):
super(Resolver, self).__init__()
assert upgrade_strategy in self._allowed_strategies
self.preparer = preparer
self.finder = finder
self.session = session
# NOTE: This would eventually be replaced with a cache that can give
# information about both sdist and wheels transparently.
self.wheel_cache = wheel_cache
self.require_hashes = None # This is set in resolve
self.upgrade_strategy = upgrade_strategy
self.force_reinstall = force_reinstall
self.isolated = isolated
self.ignore_dependencies = ignore_dependencies
self.ignore_installed = ignore_installed
self.ignore_requires_python = ignore_requires_python
self.ignore_compatibility = ignore_requires_python
self.use_user_site = use_user_site
self._discovered_dependencies = defaultdict(list)
def resolve(self, requirement_set):
"""Resolve what operations need to be done
As a side-effect of this method, the packages (and their dependencies)
are downloaded, unpacked and prepared for installation. This
preparation is done by ``pip.operations.prepare``.
Once PyPI has static dependency metadata available, it would be
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
locations = self.finder.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
# req.populate_link() needs to be called before we can make decisions
# based on link type.
discovered_reqs = []
hash_errors = HashErrors()
for req in chain(root_reqs, discovered_reqs):
try:
discovered_reqs.extend(
self._resolve_one(requirement_set, req)
)
except HashError as exc:
exc.req = req
hash_errors.append(exc)
if hash_errors:
raise hash_errors
def _is_upgrade_allowed(self, req):
if self.upgrade_strategy == "to-satisfy-only":
return False
elif self.upgrade_strategy == "eager":
return True
else:
assert self.upgrade_strategy == "only-if-needed"
return req.is_direct
def _set_req_to_reinstall(self, req):
"""
Set a requirement to be installed.
"""
# 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.satisfied_by = None
# XXX: Stop passing requirement_set for options
def _check_skip_installed(self, req_to_install):
"""Check if req_to_install should be skipped.
This will check if the req is installed, and whether we should upgrade
or reinstall it, taking into account all the relevant user options.
After calling this req_to_install will only have satisfied_by set to
None if the req_to_install is to be upgraded/reinstalled etc. Any
other value will be a dist recording the current thing installed that
satisfies the requirement.
Note that for vcs urls and the like we can't assess skipping in this
routine - we simply identify that we need to pull the thing down,
then later on it is pulled down and introspected to assess upgrade/
reinstalls etc.
:return: A text reason for why it was skipped, or None.
"""
if self.ignore_installed:
return None
req_to_install.check_if_exists(self.use_user_site)
if not req_to_install.satisfied_by:
return None
if self.force_reinstall:
self._set_req_to_reinstall(req_to_install)
return None
if not self._is_upgrade_allowed(req_to_install):
if self.upgrade_strategy == "only-if-needed":
return 'not upgraded as not directly required'
return 'already satisfied'
# Check for the possibility of an upgrade. For link-based
# requirements we have to pull the tree down and inspect to assess
# the version #, so it's handled way down.
if not req_to_install.link:
try:
self.finder.find_requirement(req_to_install, upgrade=True)
except BestVersionAlreadyInstalled:
# Then the best version is installed.
return 'already up-to-date'
except DistributionNotFound:
# No distribution found, so we squash the error. It will
# be raised later when we re-try later to do the install.
# Why don't we just raise here?
pass
self._set_req_to_reinstall(req_to_install)
return None
def _get_abstract_dist_for(self, req):
"""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,
)
# satisfied_by is only evaluated by calling _check_skip_installed,
# so it must be None here.
assert req.satisfied_by is None
skip_reason = self._check_skip_installed(req)
if req.satisfied_by:
return self.preparer.prepare_installed_requirement(
req, self.require_hashes, skip_reason
)
upgrade_allowed = self._is_upgrade_allowed(req)
abstract_dist = self.preparer.prepare_linked_requirement(
req, self.session, self.finder, upgrade_allowed,
self.require_hashes
)
# NOTE
# The following portion is for determining if a certain package is
# going to be re-installed/upgraded or not and reporting to the user.
# This should probably get cleaned up in a future refactor.
# req.req is only avail after unpack for URL
# pkgs repeat check_if_exists to uninstall-on-upgrade
# (#14)
if not self.ignore_installed:
req.check_if_exists(self.use_user_site)
if req.satisfied_by:
should_modify = (
self.upgrade_strategy != "to-satisfy-only" or
self.force_reinstall or
self.ignore_installed or
req.link.scheme == 'file'
)
if should_modify:
self._set_req_to_reinstall(req)
else:
logger.info(
'Requirement already satisfied (use --upgrade to upgrade):'
' %s', req,
)
return abstract_dist
def _resolve_one(self, requirement_set, req_to_install, ignore_requires_python=False):
"""Prepare a single requirements file.
:return: A list of additional InstallRequirements to also install.
"""
# Tell user what we are doing for this requirement:
# obtain (editable), skipping, processing (local url), collecting
# (remote url or package name)
if ignore_requires_python or self.ignore_requires_python:
self.ignore_compatibility = True
if req_to_install.constraint or req_to_install.prepared:
return []
req_to_install.prepared = True
# register tmp src for cleanup in case something goes wrong
requirement_set.reqs_to_cleanup.append(req_to_install)
abstract_dist = self._get_abstract_dist_for(req_to_install)
# Parse and return dependencies
dist = abstract_dist.dist(self.finder)
try:
check_dist_requires_python(dist)
except UnsupportedPythonVersion as err:
if self.ignore_compatibility:
logger.warning(err.args[0])
else:
raise
# A huge hack, by Kenneth Reitz.
try:
self.requires_python = check_dist_requires_python(dist, absorb=False)
except TypeError:
self.requires_python = None
more_reqs = []
def add_req(subreq, extras_requested):
sub_install_req = InstallRequirement.from_req(
str(subreq),
req_to_install,
isolated=self.isolated,
wheel_cache=self.wheel_cache,
)
parent_req_name = req_to_install.name
to_scan_again, add_to_parent = requirement_set.add_requirement(
sub_install_req,
parent_req_name=parent_req_name,
extras_requested=extras_requested,
)
if parent_req_name and add_to_parent:
self._discovered_dependencies[parent_req_name].append(
add_to_parent
)
more_reqs.extend(to_scan_again)
with indent_log():
# We add req_to_install before its dependencies, so that we
# can refer to it when adding dependencies.
if not requirement_set.has_requirement(req_to_install.name):
available_requested = sorted(
set(dist.extras) & set(req_to_install.extras)
)
# 'unnamed' requirements will get added here
req_to_install.is_direct = True
requirement_set.add_requirement(
req_to_install, parent_req_name=None,
extras_requested=available_requested,
)
if not self.ignore_dependencies:
if req_to_install.extras:
logger.debug(
"Installing extra requirements: %r",
','.join(req_to_install.extras),
)
missing_requested = sorted(
set(req_to_install.extras) - set(dist.extras)
)
for missing in missing_requested:
logger.warning(
'%s does not provide the extra \'%s\'',
dist, missing
)
available_requested = sorted(
set(dist.extras) & set(req_to_install.extras)
)
for subreq in dist.requires(available_requested):
add_req(subreq, extras_requested=available_requested)
# Hack for deep-resolving extras.
for available in available_requested:
if hasattr(dist, '_DistInfoDistribution__dep_map'):
for req in dist._DistInfoDistribution__dep_map[available]:
req = InstallRequirement.from_req(
str(req),
req_to_install,
isolated=self.isolated,
wheel_cache=self.wheel_cache,
)
more_reqs.append(req)
if not req_to_install.editable and not req_to_install.satisfied_by:
# XXX: --no-install leads this to report 'Successfully
# downloaded' for only non-editable reqs, even though we took
# action on them.
requirement_set.successfully_downloaded.append(req_to_install)
return more_reqs
def get_installation_order(self, req_set):
"""Create the installation order.
The installation order is topological - requirements are installed
before the requiring thing. We break cycles at an arbitrary point,
and make no other guarantees.
"""
# The current implementation, which we may change at any point
# installs the user specified things in the order given, except when
# dependencies must come earlier to achieve topological order.
order = []
ordered_reqs = set()
def schedule(req):
if req.satisfied_by or req in ordered_reqs:
return
if req.constraint:
return
ordered_reqs.add(req)
for dep in self._discovered_dependencies[req.name]:
schedule(dep)
order.append(req)
for install_req in req_set.requirements.values():
schedule(install_req)
return order
@@ -7,8 +7,9 @@ from __future__ import absolute_import
import os
import sys
from pip9.compat import WINDOWS, expanduser
from pip9._vendor.six import PY2, text_type
from notpip._vendor.six import PY2, text_type
from notpip._internal.compat import WINDOWS, expanduser
def user_cache_dir(appname):
@@ -60,7 +61,7 @@ def user_cache_dir(appname):
def user_data_dir(appname, roaming=False):
"""
r"""
Return full path to the user-specific data dir for this application.
"appname" is the name of application.
@@ -74,6 +75,7 @@ def user_data_dir(appname, roaming=False):
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>\ ...
@@ -93,6 +95,13 @@ def user_data_dir(appname, roaming=False):
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(
@@ -137,7 +146,7 @@ def user_config_dir(appname, roaming=True):
# for the discussion regarding site_config_dirs locations
# see <https://github.com/pypa/pip/issues/1733>
def site_config_dirs(appname):
"""Return a list of potential user-shared config dirs for this application.
r"""Return a list of potential user-shared config dirs for this application.
"appname" is the name of application.
@@ -222,6 +231,7 @@ def _get_win_folder_with_ctypes(csidl_name):
return buf.value
if WINDOWS:
try:
import ctypes
@@ -6,6 +6,11 @@ from __future__ import absolute_import
import logging
import warnings
from notpip._internal.utils.typing import MYPY_CHECK_RUNNING
if MYPY_CHECK_RUNNING:
from typing import Any
class PipDeprecationWarning(Warning):
pass
@@ -15,22 +20,18 @@ class Pending(object):
pass
class RemovedInPip10Warning(PipDeprecationWarning):
class RemovedInPip11Warning(PipDeprecationWarning):
pass
class RemovedInPip11Warning(PipDeprecationWarning, Pending):
pass
class Python26DeprecationWarning(PipDeprecationWarning):
class RemovedInPip12Warning(PipDeprecationWarning, Pending):
pass
# Warnings <-> Logging Integration
_warnings_showwarning = None
_warnings_showwarning = None # type: Any
def _showwarning(message, category, filename, lineno, file=None, line=None):
@@ -42,8 +43,8 @@ def _showwarning(message, category, filename, lineno, file=None, line=None):
else:
if issubclass(category, PipDeprecationWarning):
# We use a specially named logger which will handle all of the
# deprecation messages for pip9.
logger = logging.getLogger("pip9.deprecations")
# deprecation messages for pip.
logger = logging.getLogger("pip._internal.deprecations")
# This is purposely using the % formatter here instead of letting
# the logging module handle the interpolation. This is because we
@@ -53,7 +54,7 @@ def _showwarning(message, category, filename, lineno, file=None, line=None):
# PipDeprecationWarnings that are Pending still have at least 2
# versions to go until they are removed so they can just be
# warnings. Otherwise, they will be removed in the very next
# version of pip9. We want these to be more obvious so we use the
# version of pip. We want these to be more obvious so we use the
# ERROR logging level.
if issubclass(category, Pending):
logger.warning(log_message)
@@ -1,7 +1,7 @@
import codecs
import locale
import re
import sys
BOMS = [
(codecs.BOM_UTF8, 'utf8'),
@@ -13,7 +13,7 @@ BOMS = [
(codecs.BOM_UTF32_LE, 'utf32-le'),
]
ENCODING_RE = re.compile(b'coding[:=]\s*([-\w.]+)')
ENCODING_RE = re.compile(br'coding[:=]\s*([-\w.]+)')
def auto_decode(data):
@@ -28,4 +28,6 @@ def auto_decode(data):
if line[0:1] == b'#' and ENCODING_RE.search(line):
encoding = ENCODING_RE.search(line).groups()[0].decode('ascii')
return data.decode(encoding)
return data.decode(locale.getpreferredencoding(False))
return data.decode(
locale.getpreferredencoding(False) or sys.getdefaultencoding(),
)
@@ -1,7 +1,7 @@
import os
import os.path
from pip9.compat import get_path_uid
from notpip._internal.compat import get_path_uid
def check_path_owner(path):
@@ -1,8 +1,7 @@
from __future__ import absolute_import
import re
import ctypes
import platform
import re
import warnings
@@ -73,9 +72,13 @@ def have_compatible_glibc(required_major, minimum_minor):
# misleading. Solution: instead of using platform, use our code that actually
# works.
def libc_ver():
"""Try to determine the glibc version
Returns a tuple of strings (lib, version) which default to empty strings
in case the lookup fails.
"""
glibc_version = glibc_version_string()
if glibc_version is None:
# For non-glibc platforms, fall back on platform.libc_ver
return platform.libc_ver()
return ("", "")
else:
return ("glibc", glibc_version)
@@ -2,10 +2,12 @@ from __future__ import absolute_import
import hashlib
from pip9.exceptions import HashMismatch, HashMissing, InstallationError
from pip9.utils import read_chunks
from pip9._vendor.six import iteritems, iterkeys, itervalues
from notpip._vendor.six import iteritems, iterkeys, itervalues
from notpip._internal.exceptions import (
HashMismatch, HashMissing, InstallationError,
)
from notpip._internal.utils.misc import read_chunks
# The recommended hash algo of the moment. Change this whenever the state of
# the art changes; it won't hurt backward compatibility.
@@ -5,16 +5,17 @@ import logging
import logging.handlers
import os
from notpip._internal.compat import WINDOWS
from notpip._internal.utils.misc import ensure_dir
try:
import threading
except ImportError:
import dummy_threading as threading
import dummy_threading as threading # type: ignore
from pip9.compat import WINDOWS
from pip9.utils import ensure_dir
try:
from pip9._vendor import colorama
from notpip._vendor import colorama
# Lots of different errors can come from this, including SystemError and
# ImportError.
except Exception:
@@ -75,15 +76,16 @@ class ColorizedStreamHandler(logging.StreamHandler):
else:
COLORS = []
def __init__(self, stream=None):
def __init__(self, stream=None, no_color=None):
logging.StreamHandler.__init__(self, stream)
self._no_color = no_color
if WINDOWS and colorama:
self.stream = colorama.AnsiToWin32(self.stream)
def should_color(self):
# Don't colorize things if we do not have colorama
if not colorama:
# Don't colorize things if we do not have colorama or if told not to
if not colorama or self._no_color:
return False
real_stream = (
@@ -1,6 +1,5 @@
from __future__ import absolute_import
from collections import deque
import contextlib
import errno
import io
@@ -8,26 +7,30 @@ import locale
# we have a submodule named 'logging' which would shadow this if we used the
# regular name:
import logging as std_logging
import re
import os
import posixpath
import re
import shutil
import stat
import subprocess
import sys
import tarfile
import zipfile
from collections import deque
from pip9.exceptions import InstallationError
from pip9.compat import console_to_str, expanduser, stdlib_pkgs
from pip9.locations import (
site_packages, user_site, running_under_virtualenv, virtualenv_no_global,
from notpip._vendor import pkg_resources
# NOTE: retrying is not annotated in typeshed as on 2017-07-17, which is
# why we ignore the type on this import.
from notpip._vendor.retrying import retry # type: ignore
from notpip._vendor.six import PY2
from notpip._vendor.six.moves import input
from notpip._internal.compat import console_to_str, expanduser, stdlib_pkgs
from notpip._internal.exceptions import InstallationError
from notpip._internal.locations import (
running_under_virtualenv, site_packages, user_site, virtualenv_no_global,
write_delete_marker_file,
)
from pip9._vendor import pkg_resources
from pip9._vendor.six.moves import input
from pip9._vendor.six import PY2
from pip9._vendor.retrying import retry
if PY2:
from io import BytesIO as StringIO
@@ -40,7 +43,7 @@ __all__ = ['rmtree', 'display_path', 'backup_dir',
'is_svn_page', 'file_contents',
'split_leading_dir', 'has_leading_dir',
'normalize_path',
'renames', 'get_terminal_size', 'get_prog',
'renames', 'get_prog',
'unzip_file', 'untar_file', 'unpack_file', 'call_subprocess',
'captured_stdout', 'ensure_dir',
'ARCHIVE_EXTENSIONS', 'SUPPORTED_EXTENSIONS',
@@ -88,8 +91,11 @@ def ensure_dir(path):
def get_prog():
try:
if os.path.basename(sys.argv[0]) in ('__main__.py', '-c'):
return "%s -m pip" % os.environ['PIP_PYTHON_PATH']
prog = os.path.basename(sys.argv[0])
if prog in ('__main__.py', '-c'):
return "%s -m pip" % os.environ.get('PIP_PYTHON_PATH', sys.executable)
else:
return prog
except (AttributeError, TypeError, IndexError):
pass
return 'pip'
@@ -308,7 +314,7 @@ def dist_in_usersite(dist):
def dist_in_site_packages(dist):
"""
Return True if given Distribution is installed in
distutils.sysconfig.get_python_lib().
sysconfig.get_python_lib().
"""
return normalize_path(
dist_location(dist)
@@ -338,7 +344,7 @@ def get_installed_distributions(local_only=True,
``skip`` argument is an iterable of lower-case project names to
ignore; defaults to stdlib_pkgs
If ``editables`` is False, don't report editables.
If ``include_editables`` is False, don't report editables.
If ``editables_only`` is True , only report editables.
@@ -432,36 +438,6 @@ def dist_location(dist):
return dist.location
def get_terminal_size():
"""Returns a tuple (x, y) representing the width(x) and the height(x)
in characters of the terminal window."""
def ioctl_GWINSZ(fd):
try:
import fcntl
import termios
import struct
cr = struct.unpack(
'hh',
fcntl.ioctl(fd, termios.TIOCGWINSZ, '1234')
)
except:
return None
if cr == (0, 0):
return None
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:
pass
if not cr:
cr = (os.environ.get('LINES', 25), os.environ.get('COLUMNS', 80))
return int(cr[1]), int(cr[0])
def current_umask():
"""Get the current umask which involves having to set it temporarily."""
mask = os.umask(0)
@@ -606,7 +582,7 @@ def unpack_file(filename, location, content_type, link):
elif (content_type and content_type.startswith('text/html') and
is_svn_page(file_contents(filename))):
# We don't really care about this
from pip9.vcs.subversion import Subversion
from notpip._internal.vcs.subversion import Subversion
Subversion('svn+' + link.url).unpack(location)
else:
# FIXME: handle?
@@ -624,7 +600,14 @@ def unpack_file(filename, location, content_type, link):
def call_subprocess(cmd, show_stdout=True, cwd=None,
on_returncode='raise',
command_desc=None,
extra_environ=None, spinner=None):
extra_environ=None, unset_environ=None, spinner=None):
"""
Args:
unset_environ: an iterable of environment variable names to unset
prior to calling subprocess.Popen().
"""
if unset_environ is None:
unset_environ = []
# This function's handling of subprocess output is confusing and I
# previously broke it terribly, so as penance I will write a long comment
# explaining things.
@@ -661,17 +644,21 @@ def call_subprocess(cmd, show_stdout=True, cwd=None,
env = os.environ.copy()
if extra_environ:
env.update(extra_environ)
for name in unset_environ:
env.pop(name, None)
try:
proc = subprocess.Popen(
cmd, stderr=subprocess.STDOUT, stdin=None, stdout=stdout,
cwd=cwd, env=env)
cmd, stderr=subprocess.STDOUT, stdin=subprocess.PIPE,
stdout=stdout, cwd=cwd, env=env,
)
proc.stdin.close()
except Exception as exc:
logger.critical(
"Error %s while executing command %s", exc, command_desc,
)
raise
all_output = []
if stdout is not None:
all_output = []
while True:
line = console_to_str(proc.stdout.readline())
if not line:
@@ -685,7 +672,11 @@ def call_subprocess(cmd, show_stdout=True, cwd=None,
# Update the spinner
if spinner is not None:
spinner.spin()
proc.wait()
try:
proc.wait()
finally:
if proc.stdout:
proc.stdout.close()
if spinner is not None:
if proc.returncode:
spinner.finish("error")
@@ -850,3 +841,11 @@ def get_installed_version(dist_name, lookup_dirs=None):
def consume(iterator):
"""Consume an iterable at C speed."""
deque(iterator, maxlen=0)
# Simulates an enum
def enum(*sequential, **named):
enums = dict(zip(sequential, range(len(sequential))), **named)
reverse = {value: key for key, value in enums.items()}
enums['reverse_mapping'] = reverse
return type('Enum', (), enums)
@@ -6,15 +6,14 @@ import logging
import os.path
import sys
from pip9._vendor import lockfile
from pip9._vendor.packaging import version as packaging_version
from pip9.compat import total_seconds, WINDOWS
from pip9.models import PyPI
from pip9.locations import USER_CACHE_DIR, running_under_virtualenv
from pip9.utils import ensure_dir, get_installed_version
from pip9.utils.filesystem import check_path_owner
from notpip._vendor import lockfile
from notpip._vendor.packaging import version as packaging_version
from notpip._internal.compat import WINDOWS
from notpip._internal.index import PackageFinder
from notpip._internal.locations import USER_CACHE_DIR, running_under_virtualenv
from notpip._internal.utils.filesystem import check_path_owner
from notpip._internal.utils.misc import ensure_dir, get_installed_version
SELFCHECK_DATE_FMT = "%Y-%m-%dT%H:%M:%SZ"
@@ -92,15 +91,15 @@ def load_selfcheck_statefile():
return GlobalSelfCheckState()
def pip_version_check(session):
"""Check for an update for pip9.
def pip_version_check(session, options):
"""Check for an update for pip.
Limit the frequency of checks to once per week. State is stored either in
the active virtualenv or in the user's USER_CACHE_DIR keyed off the prefix
of the pip script path.
"""
installed_version = get_installed_version("pip")
if installed_version is None:
if not installed_version:
return
pip_version = packaging_version.parse(installed_version)
@@ -116,23 +115,26 @@ def pip_version_check(session):
state.state["last_check"],
SELFCHECK_DATE_FMT
)
if total_seconds(current_time - last_check) < 7 * 24 * 60 * 60:
if (current_time - last_check).total_seconds() < 7 * 24 * 60 * 60:
pypi_version = state.state["pypi_version"]
# Refresh the version if we need to or just see if we need to warn
if pypi_version is None:
resp = session.get(
PyPI.pip_json_url,
headers={"Accept": "application/json"},
# Lets use PackageFinder to see what the latest pip version is
finder = PackageFinder(
find_links=options.find_links,
index_urls=[options.index_url] + options.extra_index_urls,
allow_all_prereleases=False, # Explicitly set to False
trusted_hosts=options.trusted_hosts,
process_dependency_links=options.process_dependency_links,
session=session,
)
all_candidates = finder.find_all_candidates("pip")
if not all_candidates:
return
pypi_version = str(
max(all_candidates, key=lambda c: c.version).version
)
resp.raise_for_status()
pypi_version = [
v for v in sorted(
list(resp.json()["releases"]),
key=packaging_version.parse,
)
if not packaging_version.parse(v).is_prerelease
][-1]
# save that we've performed a check
state.save(pypi_version, current_time)
@@ -143,7 +145,7 @@ def pip_version_check(session):
if (pip_version < remote_version and
pip_version.base_version != remote_version.base_version):
# Advise "python -m pip" on Windows to avoid issues
# with overwriting pip9.exe.
# with overwriting pip.exe.
if WINDOWS:
pip_cmd = "python -m pip"
else:
@@ -154,7 +156,6 @@ def pip_version_check(session):
"'%s install --upgrade pip' command.",
pip_version, pypi_version, pip_cmd
)
except Exception:
logger.debug(
"There was an error checking the latest version of pip",
@@ -1,19 +1,18 @@
from __future__ import absolute_import
from email.parser import FeedParser
import logging
import sys
import os
import sys
from email.parser import FeedParser # type: ignore
from pip9._vendor.packaging import specifiers
from pip9._vendor.packaging import version
from pip9._vendor import pkg_resources
from notpip._vendor import pkg_resources
from notpip._vendor.packaging import specifiers, version
from pip9 import exceptions
from notpip._internal import exceptions
logger = logging.getLogger(__name__)
def check_requires_python(requires_python):
"""
Check if the python version in use match the `requires_python` specifier.
@@ -28,6 +27,7 @@ def check_requires_python(requires_python):
# The package provides no information
return True
requires_python_specifier = specifiers.SpecifierSet(requires_python)
# We only use major.minor.micro
python_version = version.parse('{0}.{1}.{2}'.format(*sys.version_info[:3]))
return python_version in requires_python_specifier
@@ -49,19 +49,26 @@ def check_dist_requires_python(dist, absorb=True):
requires_python = pkg_info_dict.get('Requires-Python')
if not absorb:
return requires_python
try:
if not check_requires_python(requires_python):
# raise exceptions.UnsupportedPythonVersion(
# "%s requires Python '%s' but the running Python is %s" % (
# dist.project_name,
# requires_python,
# '{0}.{1}.{2}'.format(*sys.version_info[:3])
# )
# '.'.join(map(str, sys.version_info[:3])),)
# )
return
except specifiers.InvalidSpecifier as e:
logger.warning(
"Package %s has an invalid Requires-Python entry %s - %s" % (
dist.project_name, requires_python, e))
"Package %s has an invalid Requires-Python entry %s - %s",
dist.project_name, requires_python, e,
)
return
def get_installer(dist):
if dist.has_metadata('INSTALLER'):
for line in dist.get_metadata_lines('INSTALLER'):
if line.strip():
return line.strip()
return ''
@@ -0,0 +1,82 @@
from __future__ import absolute_import
import logging
import os.path
import tempfile
from notpip._internal.utils.misc import rmtree
logger = logging.getLogger(__name__)
class TempDirectory(object):
"""Helper class that owns and cleans up a temporary directory.
This class can be used as a context manager or as an OO representation of a
temporary directory.
Attributes:
path
Location to the created temporary directory or None
delete
Whether the directory should be deleted when exiting
(when used as a contextmanager)
Methods:
create()
Creates a temporary directory and stores its path in the path
attribute.
cleanup()
Deletes the temporary directory and sets path attribute to None
When used as a context manager, a temporary directory is created on
entering the context and, if the delete attribute is True, on exiting the
context the created directory is deleted.
"""
def __init__(self, path=None, delete=None, kind="temp"):
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
self.path = path
self.delete = delete
self.kind = kind
def __repr__(self):
return "<{} {!r}>".format(self.__class__.__name__, self.path)
def __enter__(self):
self.create()
return self
def __exit__(self, exc, value, tb):
if self.delete:
self.cleanup()
def create(self):
"""Create a temporary directory and store it's path in self.path
"""
if self.path is not None:
logger.debug(
"Skipped creation of temporary directory: {}".format(self.path)
)
return
# We realpath here because some systems have their default tmpdir
# symlinked to another directory. This tends to confuse build
# scripts, so we canonicalize the path by traversing potential
# symlinks here.
self.path = os.path.realpath(
tempfile.mkdtemp(prefix="pip-{}-".format(self.kind))
)
logger.debug("Created temporary directory: {}".format(self.path))
def cleanup(self):
"""Remove the temporary directory created and reset state
"""
if self.path is not None and os.path.exists(self.path):
rmtree(self.path)
self.path = None
@@ -0,0 +1,29 @@
"""For neatly implementing static typing in pip.
`mypy` - the static type analysis tool we use - uses the `typing` module, which
provides core functionality fundamental to mypy's functioning.
Generally, `typing` would be imported at runtime and used in that fashion -
it acts as a no-op at runtime and does not have any run-time overhead by
design.
As it turns out, `typing` is not vendorable - it uses separate sources for
Python 2/Python 3. Thus, this codebase can not expect it to be present.
To work around this, mypy allows the typing import to be behind a False-y
optional to prevent it from running at runtime and type-comments can be used
to remove the need for the types to be accessible directly during runtime.
This module provides the False-y guard in a nicely named fashion so that a
curious maintainer can reach here to read this.
In pip, all static-typing related imports should be guarded as follows:
from notpip.utils.typing import MYPY_CHECK_RUNNING
if MYPY_CHECK_RUNNING:
from typing import ...
Ref: https://github.com/python/mypy/issues/3216
"""
MYPY_CHECK_RUNNING = False
@@ -1,24 +1,30 @@
from __future__ import absolute_import
from __future__ import division
from __future__ import absolute_import, division
import itertools
import sys
from signal import signal, SIGINT, default_int_handler
import time
import contextlib
import itertools
import logging
import sys
import time
from signal import SIGINT, default_int_handler, signal
from pip9.compat import WINDOWS
from pip9.utils import format_size
from pip9.utils.logging import get_indentation
from pip9._vendor import six
from pip9._vendor.progress.bar import Bar, IncrementalBar
from pip9._vendor.progress.helpers import (WritelnMixin,
HIDE_CURSOR, SHOW_CURSOR)
from pip9._vendor.progress.spinner import Spinner
from notpip._vendor import six
from notpip._vendor.progress.bar import (
Bar, ChargingBar, FillingCirclesBar, FillingSquaresBar, IncrementalBar,
ShadyBar,
)
from notpip._vendor.progress.helpers import HIDE_CURSOR, SHOW_CURSOR, WritelnMixin
from notpip._vendor.progress.spinner import Spinner
from notpip._internal.compat import WINDOWS
from notpip._internal.utils.logging import get_indentation
from notpip._internal.utils.misc import format_size
from notpip._internal.utils.typing import MYPY_CHECK_RUNNING
if MYPY_CHECK_RUNNING:
from typing import Any
try:
from pip9._vendor import colorama
from notpip._vendor import colorama
# Lots of different errors can come from this, including SystemError and
# ImportError.
except Exception:
@@ -54,7 +60,7 @@ def _select_progress_class(preferred, fallback):
return preferred
_BaseBar = _select_progress_class(IncrementalBar, Bar)
_BaseBar = _select_progress_class(IncrementalBar, Bar) # type: Any
class InterruptibleMixin(object):
@@ -112,6 +118,20 @@ class InterruptibleMixin(object):
self.original_handler(signum, frame)
class SilentBar(Bar):
def update(self):
pass
class BlueEmojiBar(IncrementalBar):
suffix = "%(percent)d%%"
bar_prefix = " "
bar_suffix = " "
phases = (u"\U0001F539", u"\U0001F537", u"\U0001F535") # type: Any
class DownloadProgressMixin(object):
def __init__(self, *args, **kwargs):
@@ -171,13 +191,54 @@ class WindowsMixin(object):
self.file.flush = lambda: self.file.wrapped.flush()
class DownloadProgressBar(WindowsMixin, InterruptibleMixin,
DownloadProgressMixin, _BaseBar):
class BaseDownloadProgressBar(WindowsMixin, InterruptibleMixin,
DownloadProgressMixin):
file = sys.stdout
message = "%(percent)d%%"
suffix = "%(downloaded)s %(download_speed)s %(pretty_eta)s"
# NOTE: The "type: ignore" comments on the following classes are there to
# work around https://github.com/python/typing/issues/241
class DefaultDownloadProgressBar(BaseDownloadProgressBar,
_BaseBar): # type: ignore
pass
class DownloadSilentBar(BaseDownloadProgressBar, SilentBar): # type: ignore
pass
class DownloadIncrementalBar(BaseDownloadProgressBar, # type: ignore
IncrementalBar):
pass
class DownloadChargingBar(BaseDownloadProgressBar, # type: ignore
ChargingBar):
pass
class DownloadShadyBar(BaseDownloadProgressBar, ShadyBar): # type: ignore
pass
class DownloadFillingSquaresBar(BaseDownloadProgressBar, # type: ignore
FillingSquaresBar):
pass
class DownloadFillingCirclesBar(BaseDownloadProgressBar, # type: ignore
FillingCirclesBar):
pass
class DownloadBlueEmojiProgressBar(BaseDownloadProgressBar, # type: ignore
BlueEmojiBar):
pass
class DownloadProgressSpinner(WindowsMixin, InterruptibleMixin,
DownloadProgressMixin, WritelnMixin, Spinner):
@@ -205,6 +266,22 @@ class DownloadProgressSpinner(WindowsMixin, InterruptibleMixin,
self.writeln(line)
BAR_TYPES = {
"off": (DownloadSilentBar, DownloadSilentBar),
"on": (DefaultDownloadProgressBar, DownloadProgressSpinner),
"ascii": (DownloadIncrementalBar, DownloadProgressSpinner),
"pretty": (DownloadFillingCirclesBar, DownloadProgressSpinner),
"emoji": (DownloadBlueEmojiProgressBar, DownloadProgressSpinner)
}
def DownloadProgressProvider(progress_bar, max=None):
if max is None or max == 0:
return BAR_TYPES[progress_bar][1]().iter
else:
return BAR_TYPES[progress_bar][0](max=max).iter
################################################################
# Generic "something is happening" spinners
#
@@ -1,18 +1,24 @@
"""Handles all VCS (version control) support"""
from __future__ import absolute_import
import copy
import errno
import logging
import os
import shutil
import sys
from pip9._vendor.six.moves.urllib import parse as urllib_parse
from notpip._vendor.six.moves.urllib import parse as urllib_parse
from pip9.exceptions import BadCommand
from pip9.utils import (display_path, backup_dir, call_subprocess,
rmtree, ask_path_exists)
from notpip._internal.exceptions import BadCommand
from notpip._internal.utils.misc import (
display_path, backup_dir, call_subprocess, rmtree, ask_path_exists,
)
from notpip._internal.utils.typing import MYPY_CHECK_RUNNING
if MYPY_CHECK_RUNNING:
from typing import Dict, Optional, Tuple
from notpip._internal.basecommand import Command
__all__ = ['vcs', 'get_src_requirement']
@@ -20,8 +26,69 @@ __all__ = ['vcs', 'get_src_requirement']
logger = logging.getLogger(__name__)
class RevOptions(object):
"""
Encapsulates a VCS-specific revision to install, along with any VCS
install options.
Instances of this class should be treated as if immutable.
"""
def __init__(self, vcs, rev=None, extra_args=None):
"""
Args:
vcs: a VersionControl object.
rev: the name of the revision to install.
extra_args: a list of extra options.
"""
if extra_args is None:
extra_args = []
self.extra_args = extra_args
self.rev = rev
self.vcs = vcs
def __repr__(self):
return '<RevOptions {}: rev={!r}>'.format(self.vcs.name, self.rev)
@property
def arg_rev(self):
if self.rev is None:
return self.vcs.default_arg_rev
return self.rev
def to_args(self):
"""
Return the VCS-specific command arguments.
"""
args = []
rev = self.arg_rev
if rev is not None:
args += self.vcs.get_base_rev_args(rev)
args += self.extra_args
return args
def to_display(self):
if not self.rev:
return ''
return ' (to revision {})'.format(self.rev)
def make_new(self, rev):
"""
Make a copy of the current instance, but with a new rev.
Args:
rev: the name of the revision for the new object.
"""
return self.vcs.make_rev_options(rev, extra_args=self.extra_args)
class VcsSupport(object):
_registry = {}
_registry = {} # type: Dict[str, Command]
schemes = ['ssh', 'git', 'hg', 'bzr', 'sftp', 'svn']
def __init__(self):
@@ -98,12 +165,34 @@ class VersionControl(object):
name = ''
dirname = ''
# List of supported schemes for this Version Control
schemes = ()
schemes = () # type: Tuple[str, ...]
# Iterable of environment variable names to pass to call_subprocess().
unset_environ = () # type: Tuple[str, ...]
default_arg_rev = None # type: Optional[str]
def __init__(self, url=None, *args, **kwargs):
self.url = url
super(VersionControl, self).__init__(*args, **kwargs)
def get_base_rev_args(self, rev):
"""
Return the base revision arguments for a vcs command.
Args:
rev: the name of a revision to install. Cannot be None.
"""
raise NotImplementedError
def make_rev_options(self, rev=None, extra_args=None):
"""
Return a RevOptions object.
Args:
rev: the name of a revision to install.
extra_args: a list of extra options.
"""
return RevOptions(self, rev, extra_args=extra_args)
def _is_local_repository(self, repo):
"""
posix absolute paths start with os.path.sep,
@@ -175,31 +264,44 @@ class VersionControl(object):
def switch(self, dest, url, rev_options):
"""
Switch the repo at ``dest`` to point to ``URL``.
Args:
rev_options: a RevOptions object.
"""
raise NotImplementedError
def update(self, dest, rev_options):
"""
Update an already-existing repo to the given ``rev_options``.
Args:
rev_options: a RevOptions object.
"""
raise NotImplementedError
def check_version(self, dest, rev_options):
def is_commit_id_equal(self, dest, name):
"""
Return True if the version is identical to what exists and
doesn't need to be updated.
Return whether the id of the current commit equals the given name.
Args:
dest: the repository directory.
name: a string name.
"""
raise NotImplementedError
def check_destination(self, dest, url, rev_options, rev_display):
def check_destination(self, dest, url, rev_options):
"""
Prepare a location to receive a checkout/clone.
Return True if the location is ready for (and requires) a
checkout/clone, False otherwise.
Args:
rev_options: a RevOptions object.
"""
checkout = True
prompt = False
rev_display = rev_options.to_display()
if os.path.exists(dest):
checkout = False
if os.path.exists(os.path.join(dest, self.dirname)):
@@ -211,7 +313,7 @@ class VersionControl(object):
display_path(dest),
url,
)
if not self.check_version(dest, rev_options):
if not self.is_commit_id_equal(dest, rev_options.rev):
logger.info(
'Updating %s %s%s',
display_path(dest),
@@ -303,8 +405,7 @@ class VersionControl(object):
def get_revision(self, location):
"""
Return the current revision of the files at location
Used in get_info
Return the current commit id of the files at the given location.
"""
raise NotImplementedError
@@ -322,12 +423,16 @@ class VersionControl(object):
return call_subprocess(cmd, show_stdout, cwd,
on_returncode,
command_desc, extra_environ,
spinner)
unset_environ=self.unset_environ,
spinner=spinner)
except OSError as e:
# errno.ENOENT = no such file or directory
# In other words, the VCS executable isn't available
if e.errno == errno.ENOENT:
raise BadCommand('Cannot find command %r' % self.name)
raise BadCommand(
'Cannot find command %r - do you have '
'%r installed and in your '
'PATH?' % (self.name, self.name))
else:
raise # re-raise exception if a different error occurred
@@ -2,18 +2,13 @@ from __future__ import absolute_import
import logging
import os
import tempfile
# TODO: Get this into six.moves.urllib.parse
try:
from urllib import parse as urllib_parse
except ImportError:
import urlparse as urllib_parse
from pip9.utils import rmtree, display_path
from pip9.vcs import vcs, VersionControl
from pip9.download import path_to_url
from notpip._vendor.six.moves.urllib import parse as urllib_parse
from notpip._internal.download import path_to_url
from notpip._internal.utils.misc import display_path, rmtree
from notpip._internal.utils.temp_dir import TempDirectory
from notpip._internal.vcs import VersionControl, vcs
logger = logging.getLogger(__name__)
@@ -29,49 +24,50 @@ class Bazaar(VersionControl):
def __init__(self, url=None, *args, **kwargs):
super(Bazaar, self).__init__(url, *args, **kwargs)
# Python >= 2.7.4, 3.3 doesn't have uses_fragment or non_hierarchical
# This is only needed for python <2.7.5
# Register lp but do not expose as a scheme to support bzr+lp.
if getattr(urllib_parse, 'uses_fragment', None):
urllib_parse.uses_fragment.extend(['lp'])
urllib_parse.non_hierarchical.extend(['lp'])
def get_base_rev_args(self, rev):
return ['-r', rev]
def export(self, location):
"""
Export the Bazaar repository at the url to the destination location
"""
temp_dir = tempfile.mkdtemp('-export', 'pip-')
self.unpack(temp_dir)
# Remove the location to make sure Bazaar can export it correctly
if os.path.exists(location):
# Remove the location to make sure Bazaar can export it correctly
rmtree(location)
try:
self.run_command(['export', location], cwd=temp_dir,
show_stdout=False)
finally:
rmtree(temp_dir)
with TempDirectory(kind="export") as temp_dir:
self.unpack(temp_dir.path)
self.run_command(
['export', location],
cwd=temp_dir.path, show_stdout=False,
)
def switch(self, dest, url, rev_options):
self.run_command(['switch', url], cwd=dest)
def update(self, dest, rev_options):
self.run_command(['pull', '-q'] + rev_options, cwd=dest)
cmd_args = ['pull', '-q'] + rev_options.to_args()
self.run_command(cmd_args, cwd=dest)
def obtain(self, dest):
url, rev = self.get_url_rev()
if rev:
rev_options = ['-r', rev]
rev_display = ' (to revision %s)' % rev
else:
rev_options = []
rev_display = ''
if self.check_destination(dest, url, rev_options, rev_display):
rev_options = self.make_rev_options(rev)
if self.check_destination(dest, url, rev_options):
rev_display = rev_options.to_display()
logger.info(
'Checking out %s%s to %s',
url,
rev_display,
display_path(dest),
)
self.run_command(['branch', '-q'] + rev_options + [url, dest])
cmd_args = ['branch', '-q'] + rev_options.to_args() + [url, dest]
self.run_command(cmd_args)
def get_url_rev(self):
# hotfix the URL scheme after removing bzr+ from bzr+ssh:// readd it
@@ -95,7 +91,8 @@ class Bazaar(VersionControl):
def get_revision(self, location):
revision = self.run_command(
['revno'], show_stdout=False, cwd=location)
['revno'], show_stdout=False, cwd=location,
)
return revision.splitlines()[-1]
def get_src_requirement(self, dist, location):
@@ -108,7 +105,7 @@ class Bazaar(VersionControl):
current_rev = self.get_revision(location)
return '%s@%s#egg=%s' % (repo, current_rev, egg_project_name)
def check_version(self, dest, rev_options):
def is_commit_id_equal(self, dest, name):
"""Always assume the versions don't match"""
return False
@@ -1,18 +1,18 @@
from __future__ import absolute_import
import logging
import tempfile
import os.path
import re
from pip9.compat import samefile
from pip9.exceptions import BadCommand
from pip9._vendor.six.moves.urllib import parse as urllib_parse
from pip9._vendor.six.moves.urllib import request as urllib_request
from pip9._vendor.packaging.version import parse as parse_version
from pip9.utils import display_path, rmtree
from pip9.vcs import vcs, VersionControl
from notpip._vendor.packaging.version import parse as parse_version
from notpip._vendor.six.moves.urllib import parse as urllib_parse
from notpip._vendor.six.moves.urllib import request as urllib_request
from notpip._internal.compat import samefile
from notpip._internal.exceptions import BadCommand
from notpip._internal.utils.misc import display_path
from notpip._internal.utils.temp_dir import TempDirectory
from notpip._internal.vcs import VersionControl, vcs
urlsplit = urllib_parse.urlsplit
urlunsplit = urllib_parse.urlunsplit
@@ -21,6 +21,13 @@ urlunsplit = urllib_parse.urlunsplit
logger = logging.getLogger(__name__)
HASH_REGEX = re.compile('[a-fA-F0-9]{40}')
def looks_like_hash(sha):
return bool(HASH_REGEX.match(sha))
class Git(VersionControl):
name = 'git'
dirname = '.git'
@@ -28,6 +35,10 @@ class Git(VersionControl):
schemes = (
'git', 'git+http', 'git+https', 'git+ssh', 'git+git', 'git+file',
)
# Prevent the user's environment variables from interfering with pip:
# https://github.com/pypa/pip/issues/1130
unset_environ = ('GIT_DIR', 'GIT_WORK_TREE')
default_arg_rev = 'HEAD'
def __init__(self, url=None, *args, **kwargs):
@@ -50,11 +61,14 @@ class Git(VersionControl):
super(Git, self).__init__(url, *args, **kwargs)
def get_base_rev_args(self, rev):
return [rev]
def get_git_version(self):
VERSION_PFX = 'git version '
version = self.run_command(['version'], show_stdout=False)
if version.startswith(VERSION_PFX):
version = version[len(VERSION_PFX):]
version = version[len(VERSION_PFX):].split()[0]
else:
version = ''
# get first 3 positions of the git version becasue
@@ -65,49 +79,86 @@ class Git(VersionControl):
def export(self, location):
"""Export the Git repository at the url to the destination location"""
temp_dir = tempfile.mkdtemp('-export', 'pip-')
self.unpack(temp_dir)
try:
if not location.endswith('/'):
location = location + '/'
if not location.endswith('/'):
location = location + '/'
with TempDirectory(kind="export") as temp_dir:
self.unpack(temp_dir.path)
self.run_command(
['checkout-index', '-a', '-f', '--prefix', location],
show_stdout=False, cwd=temp_dir)
finally:
rmtree(temp_dir)
def check_rev_options(self, rev, dest, rev_options):
"""Check the revision options before checkout to compensate that tags
and branches may need origin/ as a prefix.
Returns the SHA1 of the branch or tag if found.
"""
revisions = self.get_short_refs(dest)
origin_rev = 'origin/%s' % rev
if origin_rev in revisions:
# remote branch
return [revisions[origin_rev]]
elif rev in revisions:
# a local tag or branch name
return [revisions[rev]]
else:
logger.warning(
"Could not find a tag or branch '%s', assuming commit.", rev,
show_stdout=False, cwd=temp_dir.path
)
return rev_options
def check_version(self, dest, rev_options):
def get_revision_sha(self, dest, rev):
"""
Compare the current sha to the ref. ref may be a branch or tag name,
but current rev will always point to a sha. This means that a branch
or tag will never compare as True. So this ultimately only matches
against exact shas.
Return a commit hash for the given revision if it names a remote
branch or tag. Otherwise, return None.
Args:
dest: the repository directory.
rev: the revision name.
"""
return self.get_revision(dest).startswith(rev_options[0])
# Pass rev to pre-filter the list.
output = self.run_command(['show-ref', rev], cwd=dest,
show_stdout=False, on_returncode='ignore')
refs = {}
for line in output.strip().splitlines():
try:
sha, ref = line.split()
except ValueError:
# Include the offending line to simplify troubleshooting if
# this error ever occurs.
raise ValueError('unexpected show-ref line: {!r}'.format(line))
refs[ref] = sha
branch_ref = 'refs/remotes/origin/{}'.format(rev)
tag_ref = 'refs/tags/{}'.format(rev)
return refs.get(branch_ref) or refs.get(tag_ref)
def check_rev_options(self, dest, rev_options):
"""Check the revision options before checkout.
Returns a new RevOptions object for the SHA1 of the branch or tag
if found.
Args:
rev_options: a RevOptions object.
"""
rev = rev_options.arg_rev
sha = self.get_revision_sha(dest, rev)
if sha is not None:
return rev_options.make_new(sha)
# Do not show a warning for the common case of something that has
# the form of a Git commit hash.
if not looks_like_hash(rev):
logger.warning(
"Did not find branch or tag '%s', assuming revision or ref.",
rev,
)
return rev_options
def is_commit_id_equal(self, dest, name):
"""
Return whether the current commit hash equals the given name.
Args:
dest: the repository directory.
name: a string name.
"""
if not name:
# Then avoid an unnecessary subprocess call.
return False
return self.get_revision(dest) == name
def switch(self, dest, url, rev_options):
self.run_command(['config', 'remote.origin.url', url], cwd=dest)
self.run_command(['checkout', '-q'] + rev_options, cwd=dest)
cmd_args = ['checkout', '-q'] + rev_options.to_args()
self.run_command(cmd_args, cwd=dest)
self.update_submodules(dest)
@@ -119,44 +170,47 @@ class Git(VersionControl):
else:
self.run_command(['fetch', '-q'], cwd=dest)
# Then reset to wanted revision (maybe even origin/master)
if rev_options:
rev_options = self.check_rev_options(
rev_options[0], dest, rev_options,
)
self.run_command(['reset', '--hard', '-q'] + rev_options, cwd=dest)
rev_options = self.check_rev_options(dest, rev_options)
cmd_args = ['reset', '--hard', '-q'] + rev_options.to_args()
self.run_command(cmd_args, cwd=dest)
#: update submodules
self.update_submodules(dest)
def obtain(self, dest):
url, rev = self.get_url_rev()
if rev:
rev_options = [rev]
rev_display = ' (to %s)' % rev
else:
rev_options = ['origin/master']
rev_display = ''
if self.check_destination(dest, url, rev_options, rev_display):
rev_options = self.make_rev_options(rev)
if self.check_destination(dest, url, rev_options):
rev_display = rev_options.to_display()
logger.info(
'Cloning %s%s to %s', url, rev_display, display_path(dest),
)
self.run_command(['clone', '-q', url, dest])
if rev:
rev_options = self.check_rev_options(rev, dest, rev_options)
# Only do a checkout if rev_options differs from HEAD
if not self.check_version(dest, rev_options):
self.run_command(
['checkout', '-q'] + rev_options,
cwd=dest,
)
rev_options = self.check_rev_options(dest, rev_options)
# Only do a checkout if the current commit id doesn't match
# the requested revision.
if not self.is_commit_id_equal(dest, rev_options.rev):
rev = rev_options.rev
# Only fetch the revision if it's a ref
if rev.startswith('refs/'):
self.run_command(
['fetch', '-q', url] + rev_options.to_args(),
cwd=dest,
)
# Change the revision to the SHA of the ref we fetched
rev = 'FETCH_HEAD'
self.run_command(['checkout', '-q', rev], cwd=dest)
#: repo may contain submodules
self.update_submodules(dest)
def get_url(self, location):
"""Return URL of the first remote encountered."""
remotes = self.run_command(
['config', '--get-regexp', 'remote\..*\.url'],
show_stdout=False, cwd=location)
['config', '--get-regexp', r'remote\..*\.url'],
show_stdout=False, cwd=location,
)
remotes = remotes.splitlines()
found_remote = remotes[0]
for remote in remotes:
@@ -168,53 +222,10 @@ class Git(VersionControl):
def get_revision(self, location):
current_rev = self.run_command(
['rev-parse', 'HEAD'], show_stdout=False, cwd=location)
['rev-parse', 'HEAD'], show_stdout=False, cwd=location,
)
return current_rev.strip()
def get_full_refs(self, location):
"""Yields tuples of (commit, ref) for branches and tags"""
output = self.run_command(['show-ref'],
show_stdout=False, cwd=location)
for line in output.strip().splitlines():
commit, ref = line.split(' ', 1)
yield commit.strip(), ref.strip()
def is_ref_remote(self, ref):
return ref.startswith('refs/remotes/')
def is_ref_branch(self, ref):
return ref.startswith('refs/heads/')
def is_ref_tag(self, ref):
return ref.startswith('refs/tags/')
def is_ref_commit(self, ref):
"""A ref is a commit sha if it is not anything else"""
return not any((
self.is_ref_remote(ref),
self.is_ref_branch(ref),
self.is_ref_tag(ref),
))
# Should deprecate `get_refs` since it's ambiguous
def get_refs(self, location):
return self.get_short_refs(location)
def get_short_refs(self, location):
"""Return map of named refs (branches or tags) to commit hashes."""
rv = {}
for commit, ref in self.get_full_refs(location):
ref_name = None
if self.is_ref_remote(ref):
ref_name = ref[len('refs/remotes/'):]
elif self.is_ref_branch(ref):
ref_name = ref[len('refs/heads/'):]
elif self.is_ref_tag(ref):
ref_name = ref[len('refs/tags/'):]
if ref_name is not None:
rv[ref_name] = commit
return rv
def _get_subdirectory(self, location):
"""Return the relative path of setup.py to the git repo root."""
# find the repo root
@@ -2,13 +2,13 @@ from __future__ import absolute_import
import logging
import os
import tempfile
from pip9.utils import display_path, rmtree
from pip9.vcs import vcs, VersionControl
from pip9.download import path_to_url
from pip9._vendor.six.moves import configparser
from notpip._vendor.six.moves import configparser
from notpip._internal.download import path_to_url
from notpip._internal.utils.misc import display_path
from notpip._internal.utils.temp_dir import TempDirectory
from notpip._internal.vcs import VersionControl, vcs
logger = logging.getLogger(__name__)
@@ -19,15 +19,17 @@ class Mercurial(VersionControl):
repo_name = 'clone'
schemes = ('hg', 'hg+http', 'hg+https', 'hg+ssh', 'hg+static-http')
def get_base_rev_args(self, rev):
return [rev]
def export(self, location):
"""Export the Hg repository at the url to the destination location"""
temp_dir = tempfile.mkdtemp('-export', 'pip-')
self.unpack(temp_dir)
try:
with TempDirectory(kind="export") as temp_dir:
self.unpack(temp_dir.path)
self.run_command(
['archive', location], show_stdout=False, cwd=temp_dir)
finally:
rmtree(temp_dir)
['archive', location], show_stdout=False, cwd=temp_dir.path
)
def switch(self, dest, url, rev_options):
repo_config = os.path.join(dest, self.dirname, 'hgrc')
@@ -42,21 +44,19 @@ class Mercurial(VersionControl):
'Could not switch Mercurial repository to %s: %s', url, exc,
)
else:
self.run_command(['update', '-q'] + rev_options, cwd=dest)
cmd_args = ['update', '-q'] + rev_options.to_args()
self.run_command(cmd_args, cwd=dest)
def update(self, dest, rev_options):
self.run_command(['pull', '-q'], cwd=dest)
self.run_command(['update', '-q'] + rev_options, cwd=dest)
cmd_args = ['update', '-q'] + rev_options.to_args()
self.run_command(cmd_args, cwd=dest)
def obtain(self, dest):
url, rev = self.get_url_rev()
if rev:
rev_options = [rev]
rev_display = ' (to revision %s)' % rev
else:
rev_options = []
rev_display = ''
if self.check_destination(dest, url, rev_options, rev_display):
rev_options = self.make_rev_options(rev)
if self.check_destination(dest, url, rev_options):
rev_display = rev_options.to_display()
logger.info(
'Cloning hg %s%s to %s',
url,
@@ -64,7 +64,8 @@ class Mercurial(VersionControl):
display_path(dest),
)
self.run_command(['clone', '--noupdate', '-q', url, dest])
self.run_command(['update', '-q'] + rev_options, cwd=dest)
cmd_args = ['update', '-q'] + rev_options.to_args()
self.run_command(cmd_args, cwd=dest)
def get_url(self, location):
url = self.run_command(
@@ -96,8 +97,9 @@ class Mercurial(VersionControl):
current_rev_hash = self.get_revision_hash(location)
return '%s@%s#egg=%s' % (repo, current_rev_hash, egg_project_name)
def check_version(self, dest, rev_options):
def is_commit_id_equal(self, dest, name):
"""Always assume the versions don't match"""
return False
vcs.register(Mercurial)
@@ -4,15 +4,15 @@ import logging
import os
import re
from pip9._vendor.six.moves.urllib import parse as urllib_parse
from notpip._vendor.six.moves.urllib import parse as urllib_parse
from pip9.index import Link
from pip9.utils import rmtree, display_path
from pip9.utils.logging import indent_log
from pip9.vcs import vcs, VersionControl
from notpip._internal.index import Link
from notpip._internal.utils.logging import indent_log
from notpip._internal.utils.misc import display_path, rmtree
from notpip._internal.vcs import VersionControl, vcs
_svn_xml_url_re = re.compile('url="([^"]+)"')
_svn_rev_re = re.compile('committed-rev="(\d+)"')
_svn_rev_re = re.compile(r'committed-rev="(\d+)"')
_svn_url_re = re.compile(r'URL: (.+)')
_svn_revision_re = re.compile(r'Revision: (.+)')
_svn_info_xml_rev_re = re.compile(r'\s*revision="(\d+)"')
@@ -28,6 +28,9 @@ class Subversion(VersionControl):
repo_name = 'checkout'
schemes = ('svn', 'svn+ssh', 'svn+http', 'svn+https', 'svn+svn')
def get_base_rev_args(self, rev):
return ['-r', rev]
def get_info(self, location):
"""Returns (url, revision), where both are strings"""
assert not location.rstrip('/').endswith(self.dirname), \
@@ -59,7 +62,7 @@ class Subversion(VersionControl):
def export(self, location):
"""Export the svn repository at the url to the destination location"""
url, rev = self.get_url_rev()
rev_options = get_rev_options(url, rev)
rev_options = get_rev_options(self, url, rev)
url = self.remove_auth_from_url(url)
logger.info('Exporting svn repository %s to %s', url, location)
with indent_log():
@@ -67,32 +70,31 @@ class Subversion(VersionControl):
# Subversion doesn't like to check out over an existing
# directory --force fixes this, but was only added in svn 1.5
rmtree(location)
self.run_command(
['export'] + rev_options + [url, location],
show_stdout=False)
cmd_args = ['export'] + rev_options.to_args() + [url, location]
self.run_command(cmd_args, show_stdout=False)
def switch(self, dest, url, rev_options):
self.run_command(['switch'] + rev_options + [url, dest])
cmd_args = ['switch'] + rev_options.to_args() + [url, dest]
self.run_command(cmd_args)
def update(self, dest, rev_options):
self.run_command(['update'] + rev_options + [dest])
cmd_args = ['update'] + rev_options.to_args() + [dest]
self.run_command(cmd_args)
def obtain(self, dest):
url, rev = self.get_url_rev()
rev_options = get_rev_options(url, rev)
rev_options = get_rev_options(self, url, rev)
url = self.remove_auth_from_url(url)
if rev:
rev_display = ' (to revision %s)' % rev
else:
rev_display = ''
if self.check_destination(dest, url, rev_options, rev_display):
if self.check_destination(dest, url, rev_options):
rev_display = rev_options.to_display()
logger.info(
'Checking out %s%s to %s',
url,
rev_display,
display_path(dest),
)
self.run_command(['checkout', '-q'] + rev_options + [url, dest])
cmd_args = ['checkout', '-q'] + rev_options.to_args() + [url, dest]
self.run_command(cmd_args)
def get_location(self, dist, dependency_links):
for url in dependency_links:
@@ -128,8 +130,8 @@ class Subversion(VersionControl):
dirurl, localrev = self._get_svn_url_rev(base)
if base == location:
base_url = dirurl + '/' # save the root url
elif not dirurl or not dirurl.startswith(base_url):
base = dirurl + '/' # save the root url
elif not dirurl or not dirurl.startswith(base):
dirs[:] = []
continue # not part of the same svn tree, skip it
revision = max(revision, localrev)
@@ -163,7 +165,7 @@ class Subversion(VersionControl):
return self._get_svn_url_rev(location)[0]
def _get_svn_url_rev(self, location):
from pip9.exceptions import InstallationError
from notpip._internal.exceptions import InstallationError
entries_path = os.path.join(location, self.dirname, 'entries')
if os.path.exists(entries_path):
@@ -215,7 +217,7 @@ class Subversion(VersionControl):
rev = self.get_revision(location)
return 'svn+%s@%s#egg=%s' % (repo, rev, egg_project_name)
def check_version(self, dest, rev_options):
def is_commit_id_equal(self, dest, name):
"""Always assume the versions don't match"""
return False
@@ -238,12 +240,10 @@ class Subversion(VersionControl):
return surl
def get_rev_options(url, rev):
if rev:
rev_options = ['-r', rev]
else:
rev_options = []
def get_rev_options(vcs, url, rev):
"""
Return a RevOptions object.
"""
r = urllib_parse.urlsplit(url)
if hasattr(r, 'username'):
# >= Python-2.5
@@ -259,11 +259,13 @@ def get_rev_options(url, rev):
else:
username, password = None, None
extra_args = []
if username:
rev_options += ['--username', username]
extra_args += ['--username', username]
if password:
rev_options += ['--password', password]
return rev_options
extra_args += ['--password', password]
return vcs.make_rev_options(rev, extra_args=extra_args)
vcs.register(Subversion)
@@ -3,44 +3,46 @@ Support for installing and building the "wheel" binary package format.
"""
from __future__ import absolute_import
import collections
import compileall
import copy
import csv
import errno
import functools
import hashlib
import logging
import os
import os.path
import re
import shutil
import stat
import sys
import tempfile
import warnings
from base64 import urlsafe_b64encode
from email.parser import Parser
from pip9._vendor.six import StringIO
from notpip._vendor import pkg_resources
from notpip._vendor.distlib.scripts import ScriptMaker
from notpip._vendor.packaging.utils import canonicalize_name
from notpip._vendor.six import StringIO
import pip9
from pip9.compat import expanduser
from pip9.download import path_to_url, unpack_url
from pip9.exceptions import (
InstallationError, InvalidWheelFilename, UnsupportedWheel)
from pip9.locations import distutils_scheme, PIP_DELETE_MARKER_FILENAME
from notpip import pep425tags
from pip9.utils import (
call_subprocess, ensure_dir, captured_stdout, rmtree, read_chunks,
from notpip._internal import pep425tags
from notpip._internal.build_env import BuildEnvironment
from notpip._internal.download import path_to_url, unpack_url
from notpip._internal.exceptions import (
InstallationError, InvalidWheelFilename, UnsupportedWheel,
)
from pip9.utils.ui import open_spinner
from pip9.utils.logging import indent_log
from pip9.utils.setuptools_build import SETUPTOOLS_SHIM
from pip9._vendor.distlib.scripts import ScriptMaker
from pip9._vendor import pkg_resources
from pip9._vendor.packaging.utils import canonicalize_name
from pip9._vendor.six.moves import configparser
from notpip._internal.locations import (
PIP_DELETE_MARKER_FILENAME, distutils_scheme,
)
from notpip._internal.utils.logging import indent_log
from notpip._internal.utils.misc import (
call_subprocess, captured_stdout, ensure_dir, read_chunks,
)
from notpip._internal.utils.setuptools_build import SETUPTOOLS_SHIM
from notpip._internal.utils.temp_dir import TempDirectory
from notpip._internal.utils.typing import MYPY_CHECK_RUNNING
from notpip._internal.utils.ui import open_spinner
if MYPY_CHECK_RUNNING:
from typing import Dict, List, Optional
wheel_ext = '.whl'
@@ -50,104 +52,6 @@ VERSION_COMPATIBLE = (1, 0)
logger = logging.getLogger(__name__)
class WheelCache(object):
"""A cache of wheels for future installs."""
def __init__(self, cache_dir, format_control):
"""Create a wheel cache.
:param cache_dir: The root of the cache.
:param format_control: A pip9.index.FormatControl object to limit
binaries being read from the cache.
"""
self._cache_dir = expanduser(cache_dir) if cache_dir else None
self._format_control = format_control
def cached_wheel(self, link, package_name):
return cached_wheel(
self._cache_dir, link, self._format_control, package_name)
def _cache_for_link(cache_dir, link):
"""
Return a directory to store cached wheels in for link.
Because there are M wheels for any one sdist, we provide a directory
to cache them in, and then consult that directory when looking up
cache hits.
We only insert things into the cache if they have plausible version
numbers, so that we don't contaminate the cache with things that were not
unique. E.g. ./package might have dozens of installs done for it and build
a version of 0.0...and if we built and cached a wheel, we'd end up using
the same wheel even if the source has been edited.
:param cache_dir: The cache_dir being used by pip9.
:param link: The link of the sdist for which this will cache wheels.
"""
# 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 = [link.url_without_fragment]
if link.hash_name is not None and link.hash is not None:
key_parts.append("=".join([link.hash_name, link.hash]))
key_url = "#".join(key_parts)
# 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 = hashlib.sha224(key_url.encode()).hexdigest()
# 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:]]
# Inside of the base location for cached wheels, expand our parts and join
# them all together.
return os.path.join(cache_dir, "wheels", *parts)
def cached_wheel(cache_dir, link, format_control, package_name):
if not cache_dir:
return link
if not link:
return link
if link.is_wheel:
return link
if not link.is_artifact:
return link
if not package_name:
return link
canonical_name = canonicalize_name(package_name)
formats = pip9.index.fmt_ctl_formats(format_control, canonical_name)
if "binary" not in formats:
return link
root = _cache_for_link(cache_dir, link)
try:
wheel_names = os.listdir(root)
except OSError as e:
if e.errno in (errno.ENOENT, errno.ENOTDIR):
return link
raise
candidates = []
for wheel_name in wheel_names:
try:
wheel = Wheel(wheel_name)
except InvalidWheelFilename:
continue
if not wheel.supported():
# Built for a different python/arch/etc
continue
candidates.append((wheel.support_index_min(), wheel_name))
if not candidates:
return link
candidates.sort()
path = os.path.join(root, candidates[0][1])
return pip9.index.Link(path_to_url(path))
def rehash(path, algo='sha256', blocksize=1 << 20):
"""Return (hash, length) for path using hashlib.new(algo)"""
h = hashlib.new(algo)
@@ -181,7 +85,7 @@ def fix_script(path):
firstline = script.readline()
if not firstline.startswith(b'#!python'):
return False
exename = os.environ['PIP_PYTHON_PATH'].encode(sys.getfilesystemencoding())
exename = os.environ.get('PIP_PYTHON_PATH', sys.executable).encode(sys.getfilesystemencoding())
firstline = b'#!' + exename + os.linesep.encode("ascii")
rest = script.read()
with open(path, 'wb') as script:
@@ -189,7 +93,8 @@ def fix_script(path):
script.write(rest)
return True
dist_info_re = re.compile(r"""^(?P<namever>(?P<name>.+?)(-(?P<ver>\d.+?))?)
dist_info_re = re.compile(r"""^(?P<namever>(?P<name>.+?)(-(?P<ver>.+?))?)
\.dist-info$""", re.VERBOSE)
@@ -224,21 +129,85 @@ def get_entrypoints(filename):
data.write("\n")
data.seek(0)
cp = configparser.RawConfigParser()
cp.optionxform = lambda option: option
cp.readfp(data)
# 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', {})
console = {}
gui = {}
if cp.has_section('console_scripts'):
console = dict(cp.items('console_scripts'))
if cp.has_section('gui_scripts'):
gui = dict(cp.items('gui_scripts'))
def _split_ep(s):
"""get the string representation of EntryPoint, remove space and split
on '='"""
return str(s).replace(" ", "").split("=")
# 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: (List[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]
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) for i in os.environ["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
}
if not warn_for:
return None
# Format a message
msg_lines = []
for parent_dir, scripts in warn_for.items():
scripts = sorted(scripts)
if len(scripts) == 1:
start_text = "script {} is".format(scripts[0])
else:
start_text = "scripts {} are".format(
", ".join(scripts[:-1]) + " and " + 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"))
# Returns the formatted multiline message
return "\n".join(msg_lines)
def move_wheel_files(name, req, wheeldir, user=False, home=None, root=None,
pycompile=True, scheme=None, isolated=False, prefix=None):
pycompile=True, scheme=None, isolated=False, prefix=None,
warn_script_location=True):
"""Install a wheel"""
if not scheme:
@@ -385,7 +354,7 @@ def move_wheel_files(name, req, wheeldir, user=False, home=None, root=None,
# 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 = set(('', ))
maker.variants = {''}
# This is required because otherwise distlib creates scripts that are not
# executable.
@@ -411,7 +380,7 @@ def move_wheel_files(name, req, wheeldir, user=False, home=None, root=None,
}
maker._get_script_text = _get_script_text
maker.script_template = """# -*- coding: utf-8 -*-
maker.script_template = r"""# -*- coding: utf-8 -*-
import re
import sys
@@ -488,9 +457,16 @@ if __name__ == '__main__':
# Generate the console and GUI entry points specified in the wheel
if len(console) > 0:
generated.extend(
maker.make_multiple(['%s = %s' % kv for kv in console.items()])
generated_console_scripts = maker.make_multiple(
['%s = %s' % kv for kv in console.items()]
)
generated.extend(generated_console_scripts)
if warn_script_location:
msg = message_about_scripts_not_on_PATH(generated_console_scripts)
if msg is not None:
logger.warn(msg)
if len(gui) > 0:
generated.extend(
maker.make_multiple(
@@ -527,40 +503,6 @@ if __name__ == '__main__':
shutil.move(temp_record, record)
def _unique(fn):
@functools.wraps(fn)
def unique(*args, **kw):
seen = set()
for item in fn(*args, **kw):
if item not in seen:
seen.add(item)
yield item
return unique
# TODO: this goes somewhere besides the wheel module
@_unique
def uninstallation_paths(dist):
"""
Yield all the uninstallation paths for dist based on RECORD-without-.pyc
Yield paths to all the files in RECORD. For each .py file in RECORD, add
the .pyc in the same directory.
UninstallPathSet.add() takes care of the __pycache__ .pyc.
"""
from pip9.utils import FakeFile # circular import
r = csv.reader(FakeFile(dist.get_metadata_lines('RECORD')))
for row in r:
path = os.path.join(dist.location, row[0])
yield path
if path.endswith('.py'):
dn, fn = os.path.split(path)
base = fn[:-3]
path = os.path.join(dn, base + '.pyc')
yield path
def wheel_version(source_dir):
"""
Return the Wheel-Version of an extracted wheel, if possible.
@@ -615,8 +557,8 @@ class Wheel(object):
# TODO: maybe move the install code into this class
wheel_file_re = re.compile(
r"""^(?P<namever>(?P<name>.+?)-(?P<ver>\d.*?))
((-(?P<build>\d.*?))?-(?P<pyver>.+?)-(?P<abi>.+?)-(?P<plat>.+?)
r"""^(?P<namever>(?P<name>.+?)-(?P<ver>.*?))
((-(?P<build>\d[^-]*?))?-(?P<pyver>.+?)-(?P<abi>.+?)-(?P<plat>.+?)
\.whl|\.dist-info)$""",
re.VERBOSE
)
@@ -635,15 +577,16 @@ class Wheel(object):
# 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 = set(
self.file_tags = {
(x, y, z) for x in self.pyversions
for y in self.abis for z in self.plats
)
}
def support_index_min(self, tags=None):
"""
@@ -653,41 +596,51 @@ class Wheel(object):
None is the wheel is not supported.
"""
if tags is None: # for mock
tags = pep425tags.supported_tags
tags = pep425tags.get_supported()
indexes = [tags.index(c) for c in self.file_tags if c in tags]
return min(indexes) if indexes else None
def supported(self, tags=None):
"""Is this wheel supported on this system?"""
if tags is None: # for mock
tags = pep425tags.supported_tags
tags = pep425tags.get_supported()
return bool(set(tags).intersection(self.file_tags))
class WheelBuilder(object):
"""Build wheels from a RequirementSet."""
def __init__(self, requirement_set, finder, build_options=None,
global_options=None):
self.requirement_set = requirement_set
def __init__(self, finder, preparer, wheel_cache,
build_options=None, global_options=None, no_clean=False):
self.finder = finder
self._cache_root = requirement_set._wheel_cache._cache_dir
self._wheel_dir = requirement_set.wheel_download_dir
self.preparer = preparer
self.wheel_cache = wheel_cache
self._wheel_dir = preparer.wheel_download_dir
self.build_options = build_options or []
self.global_options = global_options or []
self.no_clean = no_clean
def _build_one(self, req, output_dir, python_tag=None):
"""Build one wheel.
:return: The filename of the built wheel, or None if the build failed.
"""
tempd = tempfile.mkdtemp('pip-wheel-')
try:
if self.__build_one(req, tempd, python_tag=python_tag):
# Install build deps into temporary directory (PEP 518)
with req.build_env:
return self._build_one_inside_env(req, output_dir,
python_tag=python_tag)
def _build_one_inside_env(self, req, output_dir, python_tag=None):
with TempDirectory(kind="wheel") as temp_dir:
if self.__build_one(req, temp_dir.path, python_tag=python_tag):
try:
wheel_name = os.listdir(tempd)[0]
wheel_name = os.listdir(temp_dir.path)[0]
wheel_path = os.path.join(output_dir, wheel_name)
shutil.move(os.path.join(tempd, wheel_name), wheel_path)
shutil.move(
os.path.join(temp_dir.path, wheel_name), wheel_path
)
logger.info('Stored in directory: %s', output_dir)
return wheel_path
except:
@@ -695,12 +648,14 @@ class WheelBuilder(object):
# Ignore return, we can't do anything else useful.
self._clean_one(req)
return None
finally:
rmtree(tempd)
def _base_setup_args(self, req):
# 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.
return [
(PIP_PYTHON_PATH or sys.executable), "-u", '-c',
os.environ.get('PIP_PYTHON_PATH', sys.executable), '-u', '-c',
SETUPTOOLS_SHIM % req.setup_py
] + list(self.global_options)
@@ -737,49 +692,53 @@ class WheelBuilder(object):
logger.error('Failed cleaning build dir for %s', req.name)
return False
def build(self, autobuilding=False):
def build(self, requirements, session, autobuilding=False):
"""Build wheels.
:param unpack: If True, replace the sdist we built from with the
newly built wheel, in preparation for installation.
:return: True if all the wheels built correctly.
"""
assert self._wheel_dir or (autobuilding and self._cache_root)
# unpack sdists and constructs req set
self.requirement_set.prepare_files(self.finder)
from notpip._internal import index
reqset = self.requirement_set.requirements.values()
building_is_possible = self._wheel_dir or (
autobuilding and self.wheel_cache.cache_dir
)
assert building_is_possible
buildset = []
for req in reqset:
for req in requirements:
if req.constraint:
continue
if req.is_wheel:
if not autobuilding:
logger.info(
'Skipping %s, due to already being wheel.', req.name)
'Skipping %s, due to already being wheel.', req.name,
)
elif autobuilding and req.editable:
pass
elif autobuilding and req.link and not req.link.is_artifact:
pass
elif autobuilding and not req.source_dir:
pass
elif autobuilding and req.link and not req.link.is_artifact:
# VCS checkout. Build wheel just for this run.
buildset.append((req, True))
else:
ephem_cache = False
if autobuilding:
link = req.link
base, ext = link.splitext()
if pip9.index.egg_info_matches(base, None, link) is None:
# Doesn't look like a package - don't autobuild a wheel
# because we'll have no way to lookup the result sanely
continue
if "binary" not in pip9.index.fmt_ctl_formats(
if index.egg_info_matches(base, None, link) is None:
# E.g. local directory. Build wheel just for this run.
ephem_cache = True
if "binary" not in index.fmt_ctl_formats(
self.finder.format_control,
canonicalize_name(req.name)):
logger.info(
"Skipping bdist_wheel for %s, due to binaries "
"being disabled for it.", req.name)
"being disabled for it.", req.name,
)
continue
buildset.append(req)
buildset.append((req, ephem_cache))
if not buildset:
return True
@@ -787,15 +746,19 @@ class WheelBuilder(object):
# Build the wheels.
logger.info(
'Building wheels for collected packages: %s',
', '.join([req.name for req in buildset]),
', '.join([req.name for (req, _) in buildset]),
)
_cache = self.wheel_cache # shorter name
with indent_log():
build_success, build_failure = [], []
for req in buildset:
for req, ephem in buildset:
python_tag = None
if autobuilding:
python_tag = pep425tags.implementation_tag
output_dir = _cache_for_link(self._cache_root, req.link)
if ephem:
output_dir = _cache.get_ephem_path_for_link(req.link)
else:
output_dir = _cache.get_path_for_link(req.link)
try:
ensure_dir(output_dir)
except OSError as e:
@@ -826,15 +789,16 @@ class WheelBuilder(object):
# set the build directory again - name is known from
# the work prepare_files did.
req.source_dir = req.build_location(
self.requirement_set.build_dir)
self.preparer.build_dir
)
# Update the link for this.
req.link = pip9.index.Link(
path_to_url(wheel_file))
req.link = index.Link(path_to_url(wheel_file))
assert req.link.is_wheel
# extract the wheel into the dir
unpack_url(
req.link, req.source_dir, None, False,
session=self.requirement_set.session)
session=session,
)
else:
build_failure.append(req)
+6 -4
View File
@@ -1,8 +1,8 @@
"""
pip9._vendor is for vendoring dependencies of pip to prevent needing pip to
pip._vendor is for vendoring dependencies of pip to prevent needing pip to
depend on something external.
Files inside of pip9._vendor should be considered immutable and should only be
Files inside of pip._vendor should be considered immutable and should only be
updated to versions from upstream.
"""
from __future__ import absolute_import
@@ -13,7 +13,7 @@ import sys
# Downstream redistributors which have debundled our dependencies should also
# patch this value to be true. This will trigger the additional patching
# to cause things like "six" to be available as pip9.
# to cause things like "six" to be available as pip.
DEBUNDLED = False
# By default, look in this directory for a bunch of .whl files which we will
@@ -36,7 +36,7 @@ def vendored(modulename):
__import__(modulename, globals(), locals(), level=0)
except ImportError:
# We can just silently allow import failures to pass here. If we
# got to this point it means that ``import pip9._vendor.whatever``
# got to this point it means that ``import notpip._vendor.whatever``
# failed and so did ``import whatever``. Since we're importing this
# upfront in an attempt to alias imports, not erroring here will
# just mean we get a regular import error whenever pip *actually*
@@ -70,11 +70,13 @@ if DEBUNDLED:
vendored("six")
vendored("six.moves")
vendored("six.moves.urllib")
vendored("six.moves.urllib.parse")
vendored("packaging")
vendored("packaging.version")
vendored("packaging.specifiers")
vendored("pkg_resources")
vendored("progress")
vendored("pytoml")
vendored("retrying")
vendored("requests")
vendored("requests.packages")
+84 -32
View File
@@ -10,10 +10,10 @@ See <http://github.com/ActiveState/appdirs> for details and usage.
# Dev Notes:
# - MSDN on where to store app data files:
# http://support.microsoft.com/default.aspx?scid=kb;en-us;310294#XSLTH3194121123120121120120
# - macOS: http://developer.apple.com/documentation/MacOSX/Conceptual/BPFileSystem/index.html
# - Mac OS X: http://developer.apple.com/documentation/MacOSX/Conceptual/BPFileSystem/index.html
# - XDG spec for Un*x: http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html
__version_info__ = (1, 4, 0)
__version_info__ = (1, 4, 3)
__version__ = '.'.join(map(str, __version_info__))
@@ -30,7 +30,7 @@ if sys.platform.startswith('java'):
os_name = platform.java_ver()[3][0]
if os_name.startswith('Windows'): # "Windows XP", "Windows 7", etc.
system = 'win32'
elif os_name.startswith('Mac'): # "macOS", etc.
elif os_name.startswith('Mac'): # "Mac OS X", etc.
system = 'darwin'
else: # "Linux", "SunOS", "FreeBSD", etc.
# Setting this to "linux2" is not ideal, but only Windows or Mac
@@ -64,7 +64,7 @@ def user_data_dir(appname=None, appauthor=None, version=None, roaming=False):
for a discussion of issues.
Typical user data directories are:
macOS: ~/Library/Application Support/<AppName>
Mac OS X: ~/Library/Application Support/<AppName>
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>
@@ -98,7 +98,7 @@ def user_data_dir(appname=None, appauthor=None, version=None, roaming=False):
def site_data_dir(appname=None, appauthor=None, version=None, multipath=False):
"""Return full path to the user-shared data dir for this application.
r"""Return full path to the user-shared data dir for this application.
"appname" is the name of application.
If None, just the system directory is returned.
@@ -117,8 +117,8 @@ def site_data_dir(appname=None, appauthor=None, version=None, multipath=False):
returned, or '/usr/local/share/<AppName>',
if XDG_DATA_DIRS is not set
Typical user data directories are:
macOS: /Library/Application Support/<AppName>
Typical site data directories are:
Mac OS X: /Library/Application Support/<AppName>
Unix: /usr/local/share/<AppName> or /usr/share/<AppName>
Win XP: C:\Documents and Settings\All Users\Application Data\<AppAuthor>\<AppName>
Vista: (Fail! "C:\ProgramData" is a hidden *system* directory on Vista.)
@@ -184,13 +184,13 @@ def user_config_dir(appname=None, appauthor=None, version=None, roaming=False):
<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
Typical user config directories are:
Mac OS X: same as user_data_dir
Unix: ~/.config/<AppName> # or in $XDG_CONFIG_HOME, if defined
Win *: same as user_data_dir
For Unix, we follow the XDG spec and support $XDG_CONFIG_HOME.
That means, by deafult "~/.config/<AppName>".
That means, by default "~/.config/<AppName>".
"""
if system in ["win32", "darwin"]:
path = user_data_dir(appname, appauthor, None, roaming)
@@ -204,7 +204,7 @@ def user_config_dir(appname=None, appauthor=None, version=None, roaming=False):
def site_config_dir(appname=None, appauthor=None, version=None, multipath=False):
"""Return full path to the user-shared data dir for this application.
r"""Return full path to the user-shared data dir for this application.
"appname" is the name of application.
If None, just the system directory is returned.
@@ -222,8 +222,8 @@ def site_config_dir(appname=None, appauthor=None, version=None, multipath=False)
returned. By default, the first item from XDG_CONFIG_DIRS is
returned, or '/etc/xdg/<AppName>', if XDG_CONFIG_DIRS is not set
Typical user data directories are:
macOS: same as site_data_dir
Typical site config directories are:
Mac OS X: same as site_data_dir
Unix: /etc/xdg/<AppName> or $XDG_CONFIG_DIRS[i]/<AppName> for each value in
$XDG_CONFIG_DIRS
Win *: same as site_data_dir
@@ -273,7 +273,7 @@ def user_cache_dir(appname=None, appauthor=None, version=None, opinion=True):
discussion below.
Typical user cache directories are:
macOS: ~/Library/Caches/<AppName>
Mac OS X: ~/Library/Caches/<AppName>
Unix: ~/.cache/<AppName> (XDG default)
Win XP: C:\Documents and Settings\<username>\Local Settings\Application Data\<AppAuthor>\<AppName>\Cache
Vista: C:\Users\<username>\AppData\Local\<AppAuthor>\<AppName>\Cache
@@ -311,6 +311,48 @@ def user_cache_dir(appname=None, appauthor=None, version=None, opinion=True):
return path
def user_state_dir(appname=None, appauthor=None, version=None, roaming=False):
r"""Return full path to the user-specific state dir for this application.
"appname" is the name of application.
If None, just the system directory is returned.
"appauthor" (only used on Windows) is the name of the
appauthor or distributing body for this application. Typically
it is the owning company name. This falls back to appname. You may
pass False to disable it.
"version" is an optional version path element to append to the
path. You might want to use this if you want multiple versions
of your app to be able to run independently. If used, this
would typically be "<major>.<minor>".
Only applied when appname is present.
"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 state directories are:
Mac OS X: same as user_data_dir
Unix: ~/.local/state/<AppName> # or in $XDG_STATE_HOME, if defined
Win *: same as user_data_dir
For Unix, we follow this Debian proposal <https://wiki.debian.org/XDGBaseDirectorySpecification#state>
to extend the XDG spec and support $XDG_STATE_HOME.
That means, by default "~/.local/state/<AppName>".
"""
if system in ["win32", "darwin"]:
path = user_data_dir(appname, appauthor, None, roaming)
else:
path = os.getenv('XDG_STATE_HOME', os.path.expanduser("~/.local/state"))
if appname:
path = os.path.join(path, appname)
if appname and version:
path = os.path.join(path, version)
return path
def user_log_dir(appname=None, appauthor=None, version=None, opinion=True):
r"""Return full path to the user-specific log dir for this application.
@@ -329,8 +371,8 @@ def user_log_dir(appname=None, appauthor=None, version=None, opinion=True):
"Logs" to the base app data dir for Windows, and "log" to the
base cache dir for Unix. See discussion below.
Typical user cache directories are:
macOS: ~/Library/Logs/<AppName>
Typical user log directories are:
Mac OS X: ~/Library/Logs/<AppName>
Unix: ~/.cache/<AppName>/log # or under $XDG_CACHE_HOME if defined
Win XP: C:\Documents and Settings\<username>\Local Settings\Application Data\<AppAuthor>\<AppName>\Logs
Vista: C:\Users\<username>\AppData\Local\<AppAuthor>\<AppName>\Logs
@@ -364,8 +406,8 @@ def user_log_dir(appname=None, appauthor=None, version=None, opinion=True):
class AppDirs(object):
"""Convenience wrapper for getting application dirs."""
def __init__(self, appname, appauthor=None, version=None, roaming=False,
multipath=False):
def __init__(self, appname=None, appauthor=None, version=None,
roaming=False, multipath=False):
self.appname = appname
self.appauthor = appauthor
self.version = version
@@ -397,6 +439,11 @@ class AppDirs(object):
return user_cache_dir(self.appname, self.appauthor,
version=self.version)
@property
def user_state_dir(self):
return user_state_dir(self.appname, self.appauthor,
version=self.version)
@property
def user_log_dir(self):
return user_log_dir(self.appname, self.appauthor,
@@ -410,7 +457,10 @@ def _get_win_folder_from_registry(csidl_name):
registry for this guarantees us the correct answer for all CSIDL_*
names.
"""
import _winreg
if PY3:
import winreg as _winreg
else:
import _winreg
shell_folder_name = {
"CSIDL_APPDATA": "AppData",
@@ -500,25 +550,21 @@ def _get_win_folder_with_jna(csidl_name):
if has_high_char:
buf = array.zeros('c', buf_size)
kernel = win32.Kernel32.INSTANCE
if kernal.GetShortPathName(dir, buf, buf_size):
if kernel.GetShortPathName(dir, buf, buf_size):
dir = jna.Native.toString(buf.tostring()).rstrip("\0")
return dir
if system == "win32":
try:
import win32com.shell
_get_win_folder = _get_win_folder_with_pywin32
from ctypes import windll
_get_win_folder = _get_win_folder_with_ctypes
except ImportError:
try:
from ctypes import windll
_get_win_folder = _get_win_folder_with_ctypes
import com.sun.jna
_get_win_folder = _get_win_folder_with_jna
except ImportError:
try:
import com.sun.jna
_get_win_folder = _get_win_folder_with_jna
except ImportError:
_get_win_folder = _get_win_folder_from_registry
_get_win_folder = _get_win_folder_from_registry
#---- self test code
@@ -527,9 +573,15 @@ if __name__ == "__main__":
appname = "MyApp"
appauthor = "MyCompany"
props = ("user_data_dir", "site_data_dir",
"user_config_dir", "site_config_dir",
"user_cache_dir", "user_log_dir")
props = ("user_data_dir",
"user_config_dir",
"user_cache_dir",
"user_state_dir",
"user_log_dir",
"site_data_dir",
"site_config_dir")
print("-- app dirs %s --" % __version__)
print("-- app dirs (with optional 'version')")
dirs = AppDirs(appname, appauthor, version="1.0")
@@ -4,7 +4,7 @@ Make it easy to import from cachecontrol without long namespaces.
"""
__author__ = 'Eric Larson'
__email__ = 'eric@ionrock.org'
__version__ = '0.11.7'
__version__ = '0.12.4'
from .wrapper import CacheControl
from .adapter import CacheControlAdapter
@@ -1,10 +1,10 @@
import logging
from pip9._vendor import requests
from notpip._vendor import requests
from pip9._vendor.cachecontrol.adapter import CacheControlAdapter
from pip9._vendor.cachecontrol.cache import DictCache
from pip9._vendor.cachecontrol.controller import logger
from notpip._vendor.cachecontrol.adapter import CacheControlAdapter
from notpip._vendor.cachecontrol.cache import DictCache
from notpip._vendor.cachecontrol.controller import logger
from argparse import ArgumentParser
@@ -1,7 +1,8 @@
import types
import functools
import zlib
from pip9._vendor.requests.adapters import HTTPAdapter
from notpip._vendor.requests.adapters import HTTPAdapter
from .controller import CacheController
from .cache import DictCache
@@ -16,10 +17,12 @@ class CacheControlAdapter(HTTPAdapter):
controller_class=None,
serializer=None,
heuristic=None,
cacheable_methods=None,
*args, **kw):
super(CacheControlAdapter, self).__init__(*args, **kw)
self.cache = cache or DictCache()
self.heuristic = heuristic
self.cacheable_methods = cacheable_methods or ('GET',)
controller_factory = controller_class or CacheController
self.controller = controller_factory(
@@ -28,13 +31,17 @@ class CacheControlAdapter(HTTPAdapter):
serializer=serializer,
)
def send(self, request, **kw):
def send(self, request, cacheable_methods=None, **kw):
"""
Send a request. Use the request information to see if it
exists in the cache and cache the response if we need to and can.
"""
if request.method == 'GET':
cached_response = self.controller.cached_request(request)
cacheable = cacheable_methods or self.cacheable_methods
if request.method in cacheable:
try:
cached_response = self.controller.cached_request(request)
except zlib.error:
cached_response = None
if cached_response:
return self.build_response(request, cached_response,
from_cache=True)
@@ -48,14 +55,16 @@ class CacheControlAdapter(HTTPAdapter):
return resp
def build_response(self, request, response, from_cache=False):
def build_response(self, request, response, from_cache=False,
cacheable_methods=None):
"""
Build a response by making a request or using the cache.
This will end up calling send and returning a potentially
cached response
"""
if not from_cache and request.method == 'GET':
cacheable = cacheable_methods or self.cacheable_methods
if not from_cache and request.method in cacheable:
# Check for any heuristics that might update headers
# before trying to cache.
if self.heuristic:
@@ -1,18 +1,2 @@
from textwrap import dedent
try:
from .file_cache import FileCache
except ImportError:
notice = dedent('''
NOTE: In order to use the FileCache you must have
lockfile installed. You can install it via pip:
pip install lockfile
''')
print(notice)
try:
import redis
from .redis_cache import RedisCache
except ImportError:
pass
from .file_cache import FileCache # noqa
from .redis_cache import RedisCache # noqa
@@ -1,12 +1,16 @@
import hashlib
import os
from pip9._vendor.lockfile import LockFile
from pip9._vendor.lockfile.mkdirlockfile import MkdirLockFile
from textwrap import dedent
from ..cache import BaseCache
from ..controller import CacheController
try:
FileNotFoundError
except NameError:
# py2.X
FileNotFoundError = OSError
def _secure_open_write(filename, fmode):
# We only want to write to this file, so open it in write only mode
@@ -55,11 +59,22 @@ class FileCache(BaseCache):
if use_dir_lock is not None and lock_class is not None:
raise ValueError("Cannot use use_dir_lock and lock_class together")
if use_dir_lock:
lock_class = MkdirLockFile
try:
from notpip._vendor.lockfile import LockFile
from notpip._vendor.lockfile.mkdirlockfile import MkdirLockFile
except ImportError:
notice = dedent("""
NOTE: In order to use the FileCache you must have
lockfile installed. You can install it via pip:
pip install lockfile
""")
raise ImportError(notice)
else:
if use_dir_lock:
lock_class = MkdirLockFile
if lock_class is None:
lock_class = LockFile
elif lock_class is None:
lock_class = LockFile
self.directory = directory
self.forever = forever
@@ -67,7 +82,6 @@ class FileCache(BaseCache):
self.dirmode = dirmode
self.lock_class = lock_class
@staticmethod
def encode(x):
return hashlib.sha224(x.encode()).hexdigest()
@@ -104,7 +118,10 @@ class FileCache(BaseCache):
def delete(self, key):
name = self._fn(key)
if not self.forever:
os.remove(name)
try:
os.remove(name)
except FileNotFoundError:
pass
def url_to_file_path(url, filecache):
@@ -1,19 +1,20 @@
from __future__ import division
from datetime import datetime
from notpip._vendor.cachecontrol.cache import BaseCache
def total_seconds(td):
"""Python 2.6 compatability"""
if hasattr(td, 'total_seconds'):
return td.total_seconds()
return int(td.total_seconds())
ms = td.microseconds
secs = (td.seconds + td.days * 24 * 3600)
return (ms + secs * 10**6) / 10**6
return int((ms + secs * 10**6) / 10**6)
class RedisCache(object):
class RedisCache(BaseCache):
def __init__(self, conn):
self.conn = conn
@@ -25,7 +26,7 @@ class RedisCache(object):
if not expires:
self.conn.set(key, value)
else:
expires = expires - datetime.now()
expires = expires - datetime.utcnow()
self.conn.setex(key, total_seconds(expires), value)
def delete(self, key):
@@ -38,4 +39,5 @@ class RedisCache(object):
self.conn.delete(key)
def close(self):
self.conn.disconnect()
"""Redis uses connection pooling, no need to close the connection."""
pass
@@ -10,11 +10,20 @@ except ImportError:
import pickle
from pip9._vendor.urllib3.response import HTTPResponse
from pip9._vendor.urllib3.util import is_fp_closed
# Handle the case where the requests module has been patched to not have
# urllib3 bundled as part of its source.
try:
from notpip._vendor.requests.packages.urllib3.response import HTTPResponse
except ImportError:
from notpip._vendor.urllib3.response import HTTPResponse
try:
from notpip._vendor.requests.packages.urllib3.util import is_fp_closed
except ImportError:
from notpip._vendor.urllib3.util import is_fp_closed
# Replicate some six behaviour
try:
text_type = (unicode,)
text_type = unicode
except NameError:
text_type = (str,)
text_type = str
@@ -7,7 +7,7 @@ import calendar
import time
from email.utils import parsedate_tz
from pip9._vendor.requests.structures import CaseInsensitiveDict
from notpip._vendor.requests.structures import CaseInsensitiveDict
from .cache import DictCache
from .serialize import Serializer
@@ -30,10 +30,12 @@ def parse_uri(uri):
class CacheController(object):
"""An interface to see if request should cached or not.
"""
def __init__(self, cache=None, cache_etags=True, serializer=None):
def __init__(self, cache=None, cache_etags=True, serializer=None,
status_codes=None):
self.cache = cache or DictCache()
self.cache_etags = cache_etags
self.serializer = serializer or Serializer()
self.cacheable_status_codes = status_codes or (200, 203, 300, 301)
@classmethod
def _urlnorm(cls, uri):
@@ -60,27 +62,51 @@ class CacheController(object):
return cls._urlnorm(uri)
def parse_cache_control(self, headers):
"""
Parse the cache control headers returning a dictionary with values
for the different directives.
"""
known_directives = {
# https://tools.ietf.org/html/rfc7234#section-5.2
'max-age': (int, True,),
'max-stale': (int, False,),
'min-fresh': (int, True,),
'no-cache': (None, False,),
'no-store': (None, False,),
'no-transform': (None, False,),
'only-if-cached' : (None, False,),
'must-revalidate': (None, False,),
'public': (None, False,),
'private': (None, False,),
'proxy-revalidate': (None, False,),
's-maxage': (int, True,)
}
cc_headers = headers.get('cache-control',
headers.get('Cache-Control', ''))
retval = {}
cc_header = 'cache-control'
if 'Cache-Control' in headers:
cc_header = 'Cache-Control'
for cc_directive in cc_headers.split(','):
parts = cc_directive.split('=', 1)
directive = parts[0].strip()
try:
typ, required = known_directives[directive]
except KeyError:
logger.debug('Ignoring unknown cache-control directive: %s',
directive)
continue
if not typ or not required:
retval[directive] = None
if typ:
try:
retval[directive] = typ(parts[1].strip())
except IndexError:
if required:
logger.debug('Missing value for cache-control '
'directive: %s', directive)
except ValueError:
logger.debug('Invalid value for cache-control directive '
'%s, must be %s', directive, typ.__name__)
if cc_header in headers:
parts = headers[cc_header].split(',')
parts_with_args = [
tuple([x.strip().lower() for x in part.split("=", 1)])
for part in parts if -1 != part.find("=")
]
parts_wo_args = [
(name.strip().lower(), 1)
for name in parts if -1 == name.find("=")
]
retval = dict(parts_with_args + parts_wo_args)
return retval
def cached_request(self, request):
@@ -154,8 +180,8 @@ class CacheController(object):
freshness_lifetime = 0
# Check the max-age pragma in the cache control header
if 'max-age' in resp_cc and resp_cc['max-age'].isdigit():
freshness_lifetime = int(resp_cc['max-age'])
if 'max-age' in resp_cc:
freshness_lifetime = resp_cc['max-age']
logger.debug('Freshness lifetime from max-age: %i',
freshness_lifetime)
@@ -171,18 +197,12 @@ class CacheController(object):
# Determine if we are setting freshness limit in the
# request. Note, this overrides what was in the response.
if 'max-age' in cc:
try:
freshness_lifetime = int(cc['max-age'])
logger.debug('Freshness lifetime from request max-age: %i',
freshness_lifetime)
except ValueError:
freshness_lifetime = 0
freshness_lifetime = cc['max-age']
logger.debug('Freshness lifetime from request max-age: %i',
freshness_lifetime)
if 'min-fresh' in cc:
try:
min_fresh = int(cc['min-fresh'])
except ValueError:
min_fresh = 0
min_fresh = cc['min-fresh']
# adjust our current age by our min fresh
current_age += min_fresh
logger.debug('Adjusted current age from min-fresh: %i',
@@ -220,7 +240,8 @@ class CacheController(object):
return new_headers
def cache_response(self, request, response, body=None):
def cache_response(self, request, response, body=None,
status_codes=None):
"""
Algorithm for caching requests.
@@ -228,7 +249,7 @@ class CacheController(object):
"""
# From httplib2: Don't cache 206's since we aren't going to
# handle byte range requests
cacheable_status_codes = [200, 203, 300, 301]
cacheable_status_codes = status_codes or self.cacheable_status_codes
if response.status not in cacheable_status_codes:
logger.debug(
'Status code %s not in %s',
@@ -257,10 +278,10 @@ class CacheController(object):
# Delete it from the cache if we happen to have it stored there
no_store = False
if cc.get('no-store'):
if 'no-store' in cc:
no_store = True
logger.debug('Response header has "no-store"')
if cc_req.get('no-store'):
if 'no-store' in cc_req:
no_store = True
logger.debug('Request header has "no-store"')
if no_store and self.cache.get(cache_url):
@@ -289,13 +310,12 @@ class CacheController(object):
# the cache.
elif 'date' in response_headers:
# cache when there is a max-age > 0
if cc and cc.get('max-age'):
if cc['max-age'].isdigit() and int(cc['max-age']) > 0:
logger.debug('Caching b/c date exists and max-age > 0')
self.cache.set(
cache_url,
self.serializer.dumps(request, response, body=body),
)
if 'max-age' in cc and cc['max-age'] > 0:
logger.debug('Caching b/c date exists and max-age > 0')
self.cache.set(
cache_url,
self.serializer.dumps(request, response, body=body),
)
# If the request can expire, it means we should cache it
# in the meantime.
@@ -9,7 +9,7 @@ TIME_FMT = "%a, %d %b %Y %H:%M:%S GMT"
def expire_after(delta, date=None):
date = date or datetime.now()
date = date or datetime.utcnow()
return date + delta
@@ -3,25 +3,12 @@ import io
import json
import zlib
from pip9._vendor.requests.structures import CaseInsensitiveDict
from notpip._vendor import msgpack
from notpip._vendor.requests.structures import CaseInsensitiveDict
from .compat import HTTPResponse, pickle, text_type
def _b64_encode_bytes(b):
return base64.b64encode(b).decode("ascii")
def _b64_encode_str(s):
return _b64_encode_bytes(s.encode("utf8"))
def _b64_encode(s):
if isinstance(s, text_type):
return _b64_encode_str(s)
return _b64_encode_bytes(s)
def _b64_decode_bytes(b):
return base64.b64decode(b.encode("ascii"))
@@ -50,43 +37,40 @@ class Serializer(object):
# `Serializer.dump`.
response._fp = io.BytesIO(body)
# NOTE: This is all a bit weird, but it's really important that on
# Python 2.x these objects are unicode and not str, even when
# they contain only ascii. The problem here is that msgpack
# understands the difference between unicode and bytes and we
# have it set to differentiate between them, however Python 2
# doesn't know the difference. Forcing these to unicode will be
# enough to have msgpack know the difference.
data = {
"response": {
"body": _b64_encode_bytes(body),
"headers": dict(
(_b64_encode(k), _b64_encode(v))
u"response": {
u"body": body,
u"headers": dict(
(text_type(k), text_type(v))
for k, v in response.headers.items()
),
"status": response.status,
"version": response.version,
"reason": _b64_encode_str(response.reason),
"strict": response.strict,
"decode_content": response.decode_content,
u"status": response.status,
u"version": response.version,
u"reason": text_type(response.reason),
u"strict": response.strict,
u"decode_content": response.decode_content,
},
}
# Construct our vary headers
data["vary"] = {}
if "vary" in response_headers:
varied_headers = response_headers['vary'].split(',')
data[u"vary"] = {}
if u"vary" in response_headers:
varied_headers = response_headers[u'vary'].split(',')
for header in varied_headers:
header = header.strip()
data["vary"][header] = request.headers.get(header, None)
header_value = request.headers.get(header, None)
if header_value is not None:
header_value = text_type(header_value)
data[u"vary"][header] = header_value
# Encode our Vary headers to ensure they can be serialized as JSON
data["vary"] = dict(
(_b64_encode(k), _b64_encode(v) if v is not None else v)
for k, v in data["vary"].items()
)
return b",".join([
b"cc=2",
zlib.compress(
json.dumps(
data, separators=(",", ":"), sort_keys=True,
).encode("utf8"),
),
])
return b",".join([b"cc=4", msgpack.dumps(data, use_bin_type=True)])
def loads(self, request, data):
# Short circuit if we've been given an empty set of data
@@ -174,7 +158,7 @@ class Serializer(object):
def _loads_v2(self, request, data):
try:
cached = json.loads(zlib.decompress(data).decode("utf8"))
except ValueError:
except (ValueError, zlib.error):
return
# We need to decode the items that we've base64 encoded
@@ -194,3 +178,17 @@ class Serializer(object):
)
return self.prepare_response(request, cached)
def _loads_v3(self, request, data):
# Due to Python 2 encoding issues, it's impossible to know for sure
# exactly how to load v3 entries, thus we'll treat these as a miss so
# that they get rewritten out as v4 entries.
return
def _loads_v4(self, request, data):
try:
cached = msgpack.loads(data, encoding='utf-8')
except ValueError:
return
return self.prepare_response(request, cached)
@@ -6,14 +6,20 @@ def CacheControl(sess,
cache=None,
cache_etags=True,
serializer=None,
heuristic=None):
heuristic=None,
controller_class=None,
adapter_class=None,
cacheable_methods=None):
cache = cache or DictCache()
adapter = CacheControlAdapter(
adapter_class = adapter_class or CacheControlAdapter
adapter = adapter_class(
cache,
cache_etags=cache_etags,
serializer=serializer,
heuristic=heuristic,
controller_class=controller_class,
cacheable_methods=cacheable_methods
)
sess.mount('http://', adapter)
sess.mount('https://', adapter)
@@ -18,9 +18,9 @@ from __future__ import absolute_import, print_function, unicode_literals
import argparse
import sys
from chardet import __version__
from chardet.compat import PY2
from chardet.universaldetector import UniversalDetector
from notpip._vendor.chardet import __version__
from notpip._vendor.chardet.compat import PY2
from notpip._vendor.chardet.universaldetector import UniversalDetector
def description_of(lines, name='stdin'):
@@ -3,5 +3,5 @@ from .initialise import init, deinit, reinit, colorama_text
from .ansi import Fore, Back, Style, Cursor
from .ansitowin32 import AnsiToWin32
__version__ = '0.3.7'
__version__ = '0.3.9'
@@ -46,8 +46,8 @@ class AnsiToWin32(object):
sequences from the text, and if outputting to a tty, will convert them into
win32 function calls.
'''
ANSI_CSI_RE = re.compile('\001?\033\[((?:\d|;)*)([a-zA-Z])\002?') # Control Sequence Introducer
ANSI_OSC_RE = re.compile('\001?\033\]((?:.|;)*?)(\x07)\002?') # Operating System Command
ANSI_CSI_RE = re.compile('\001?\033\\[((?:\\d|;)*)([a-zA-Z])\002?') # Control Sequence Introducer
ANSI_OSC_RE = re.compile('\001?\033\\]((?:.|;)*?)(\x07)\002?') # Operating System Command
def __init__(self, wrapped, convert=None, strip=None, autoreset=False):
# The wrapped stream (normally sys.stdout or sys.stderr)
@@ -83,9 +83,9 @@ else:
]
_FillConsoleOutputAttribute.restype = wintypes.BOOL
_SetConsoleTitleW = windll.kernel32.SetConsoleTitleA
_SetConsoleTitleW = windll.kernel32.SetConsoleTitleW
_SetConsoleTitleW.argtypes = [
wintypes.LPCSTR
wintypes.LPCWSTR
]
_SetConsoleTitleW.restype = wintypes.BOOL
@@ -94,13 +94,15 @@ else:
STDERR: _GetStdHandle(STDERR),
}
def winapi_test():
handle = handles[STDOUT]
def _winapi_test(handle):
csbi = CONSOLE_SCREEN_BUFFER_INFO()
success = _GetConsoleScreenBufferInfo(
handle, byref(csbi))
return bool(success)
def winapi_test():
return any(_winapi_test(h) for h in handles.values())
def GetConsoleScreenBufferInfo(stream_id=STDOUT):
handle = handles[stream_id]
csbi = CONSOLE_SCREEN_BUFFER_INFO()
@@ -1,12 +1,12 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2012-2016 Vinay Sajip.
# Copyright (C) 2012-2017 Vinay Sajip.
# Licensed to the Python Software Foundation under a contributor agreement.
# See LICENSE.txt and CONTRIBUTORS.txt.
#
import logging
__version__ = '0.2.4'
__version__ = '0.2.7'
class DistlibException(Exception):
pass
@@ -1,84 +1,84 @@
[posix_prefix]
# Configuration directories. Some of these come straight out of the
# configure script. They are for implementing the other variables, not to
# be used directly in [resource_locations].
confdir = /etc
datadir = /usr/share
libdir = /usr/lib
statedir = /var
# User resource directory
local = ~/.local/{distribution.name}
stdlib = {base}/lib/python{py_version_short}
platstdlib = {platbase}/lib/python{py_version_short}
purelib = {base}/lib/python{py_version_short}/site-packages
platlib = {platbase}/lib/python{py_version_short}/site-packages
include = {base}/include/python{py_version_short}{abiflags}
platinclude = {platbase}/include/python{py_version_short}{abiflags}
data = {base}
[posix_home]
stdlib = {base}/lib/python
platstdlib = {base}/lib/python
purelib = {base}/lib/python
platlib = {base}/lib/python
include = {base}/include/python
platinclude = {base}/include/python
scripts = {base}/bin
data = {base}
[nt]
stdlib = {base}/Lib
platstdlib = {base}/Lib
purelib = {base}/Lib/site-packages
platlib = {base}/Lib/site-packages
include = {base}/Include
platinclude = {base}/Include
scripts = {base}/Scripts
data = {base}
[os2]
stdlib = {base}/Lib
platstdlib = {base}/Lib
purelib = {base}/Lib/site-packages
platlib = {base}/Lib/site-packages
include = {base}/Include
platinclude = {base}/Include
scripts = {base}/Scripts
data = {base}
[os2_home]
stdlib = {userbase}/lib/python{py_version_short}
platstdlib = {userbase}/lib/python{py_version_short}
purelib = {userbase}/lib/python{py_version_short}/site-packages
platlib = {userbase}/lib/python{py_version_short}/site-packages
include = {userbase}/include/python{py_version_short}
scripts = {userbase}/bin
data = {userbase}
[nt_user]
stdlib = {userbase}/Python{py_version_nodot}
platstdlib = {userbase}/Python{py_version_nodot}
purelib = {userbase}/Python{py_version_nodot}/site-packages
platlib = {userbase}/Python{py_version_nodot}/site-packages
include = {userbase}/Python{py_version_nodot}/Include
scripts = {userbase}/Scripts
data = {userbase}
[posix_user]
stdlib = {userbase}/lib/python{py_version_short}
platstdlib = {userbase}/lib/python{py_version_short}
purelib = {userbase}/lib/python{py_version_short}/site-packages
platlib = {userbase}/lib/python{py_version_short}/site-packages
include = {userbase}/include/python{py_version_short}
scripts = {userbase}/bin
data = {userbase}
[osx_framework_user]
stdlib = {userbase}/lib/python
platstdlib = {userbase}/lib/python
purelib = {userbase}/lib/python/site-packages
platlib = {userbase}/lib/python/site-packages
include = {userbase}/include
scripts = {userbase}/bin
data = {userbase}
[posix_prefix]
# Configuration directories. Some of these come straight out of the
# configure script. They are for implementing the other variables, not to
# be used directly in [resource_locations].
confdir = /etc
datadir = /usr/share
libdir = /usr/lib
statedir = /var
# User resource directory
local = ~/.local/{distribution.name}
stdlib = {base}/lib/python{py_version_short}
platstdlib = {platbase}/lib/python{py_version_short}
purelib = {base}/lib/python{py_version_short}/site-packages
platlib = {platbase}/lib/python{py_version_short}/site-packages
include = {base}/include/python{py_version_short}{abiflags}
platinclude = {platbase}/include/python{py_version_short}{abiflags}
data = {base}
[posix_home]
stdlib = {base}/lib/python
platstdlib = {base}/lib/python
purelib = {base}/lib/python
platlib = {base}/lib/python
include = {base}/include/python
platinclude = {base}/include/python
scripts = {base}/bin
data = {base}
[nt]
stdlib = {base}/Lib
platstdlib = {base}/Lib
purelib = {base}/Lib/site-packages
platlib = {base}/Lib/site-packages
include = {base}/Include
platinclude = {base}/Include
scripts = {base}/Scripts
data = {base}
[os2]
stdlib = {base}/Lib
platstdlib = {base}/Lib
purelib = {base}/Lib/site-packages
platlib = {base}/Lib/site-packages
include = {base}/Include
platinclude = {base}/Include
scripts = {base}/Scripts
data = {base}
[os2_home]
stdlib = {userbase}/lib/python{py_version_short}
platstdlib = {userbase}/lib/python{py_version_short}
purelib = {userbase}/lib/python{py_version_short}/site-packages
platlib = {userbase}/lib/python{py_version_short}/site-packages
include = {userbase}/include/python{py_version_short}
scripts = {userbase}/bin
data = {userbase}
[nt_user]
stdlib = {userbase}/Python{py_version_nodot}
platstdlib = {userbase}/Python{py_version_nodot}
purelib = {userbase}/Python{py_version_nodot}/site-packages
platlib = {userbase}/Python{py_version_nodot}/site-packages
include = {userbase}/Python{py_version_nodot}/Include
scripts = {userbase}/Scripts
data = {userbase}
[posix_user]
stdlib = {userbase}/lib/python{py_version_short}
platstdlib = {userbase}/lib/python{py_version_short}
purelib = {userbase}/lib/python{py_version_short}/site-packages
platlib = {userbase}/lib/python{py_version_short}/site-packages
include = {userbase}/include/python{py_version_short}
scripts = {userbase}/bin
data = {userbase}
[osx_framework_user]
stdlib = {userbase}/lib/python
platstdlib = {userbase}/lib/python
purelib = {userbase}/lib/python/site-packages
platlib = {userbase}/lib/python/site-packages
include = {userbase}/include
scripts = {userbase}/bin
data = {userbase}
@@ -221,7 +221,7 @@ def _parse_makefile(filename, vars=None):
"""
# Regexes needed for parsing Makefile (and similar syntaxes,
# like old-style Setup files).
_variable_rx = re.compile("([a-zA-Z][a-zA-Z0-9_]+)\s*=\s*(.*)")
_variable_rx = re.compile(r"([a-zA-Z][a-zA-Z0-9_]+)\s*=\s*(.*)")
_findvar1_rx = re.compile(r"\$\(([A-Za-z][A-Za-z0-9_]*)\)")
_findvar2_rx = re.compile(r"\${([A-Za-z][A-Za-z0-9_]*)}")
@@ -528,7 +528,7 @@ def get_config_vars(*args):
major_version = int(kernel_version.split('.')[0])
if major_version < 8:
# On macOS before 10.4, check if -arch and -isysroot
# On Mac OS X before 10.4, check if -arch and -isysroot
# are in CFLAGS or LDFLAGS and remove them if they are.
# This is needed when building extensions on a 10.3 system
# using a universal build of python.
@@ -537,7 +537,7 @@ def get_config_vars(*args):
# patched up as well.
'CFLAGS', 'PY_CFLAGS', 'BLDSHARED'):
flags = _CONFIG_VARS[key]
flags = re.sub('-arch\s+\w+\s', ' ', flags)
flags = re.sub(r'-arch\s+\w+\s', ' ', flags)
flags = re.sub('-isysroot [^ \t]*', ' ', flags)
_CONFIG_VARS[key] = flags
else:
@@ -554,7 +554,7 @@ def get_config_vars(*args):
'CFLAGS', 'PY_CFLAGS', 'BLDSHARED'):
flags = _CONFIG_VARS[key]
flags = re.sub('-arch\s+\w+\s', ' ', flags)
flags = re.sub(r'-arch\s+\w+\s', ' ', flags)
flags = flags + ' ' + arch
_CONFIG_VARS[key] = flags
@@ -569,7 +569,7 @@ def get_config_vars(*args):
# when you install Xcode.
#
CFLAGS = _CONFIG_VARS.get('CFLAGS', '')
m = re.search('-isysroot\s+(\S+)', CFLAGS)
m = re.search(r'-isysroot\s+(\S+)', CFLAGS)
if m is not None:
sdk = m.group(1)
if not os.path.exists(sdk):
@@ -579,7 +579,7 @@ def get_config_vars(*args):
'CFLAGS', 'PY_CFLAGS', 'BLDSHARED'):
flags = _CONFIG_VARS[key]
flags = re.sub('-isysroot\s+\S+(\s|$)', ' ', flags)
flags = re.sub(r'-isysroot\s+\S+(\s|$)', ' ', flags)
_CONFIG_VARS[key] = flags
if args:
@@ -725,7 +725,7 @@ def get_platform():
machine = 'fat'
cflags = get_config_vars().get('CFLAGS')
archs = re.findall('-arch\s+(\S+)', cflags)
archs = re.findall(r'-arch\s+(\S+)', cflags)
archs = tuple(sorted(set(archs)))
if len(archs) == 1:
+25 -16
View File
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013-2016 Vinay Sajip.
# Copyright (C) 2013-2017 Vinay Sajip.
# Licensed to the Python Software Foundation under a contributor agreement.
# See LICENSE.txt and CONTRIBUTORS.txt.
#
@@ -12,7 +12,7 @@ import sys
try:
import ssl
except ImportError:
except ImportError: # pragma: no cover
ssl = None
if sys.version_info[0] < 3: # pragma: no cover
@@ -272,7 +272,7 @@ from zipfile import ZipFile as BaseZipFile
if hasattr(BaseZipFile, '__enter__'): # pragma: no cover
ZipFile = BaseZipFile
else:
else: # pragma: no cover
from zipfile import ZipExtFile as BaseZipExtFile
class ZipExtFile(BaseZipExtFile):
@@ -329,7 +329,13 @@ try:
fsencode = os.fsencode
fsdecode = os.fsdecode
except AttributeError: # pragma: no cover
_fsencoding = sys.getfilesystemencoding()
# Issue #99: on some systems (e.g. containerised),
# sys.getfilesystemencoding() returns None, and we need a real value,
# so fall back to utf-8. From the CPython 2.7 docs relating to Unix and
# sys.getfilesystemencoding(): the return value is "the users preference
# according to the result of nl_langinfo(CODESET), or None if the
# nl_langinfo(CODESET) failed."
_fsencoding = sys.getfilesystemencoding() or 'utf-8'
if _fsencoding == 'mbcs':
_fserrors = 'strict'
else:
@@ -359,7 +365,7 @@ except ImportError: # pragma: no cover
from codecs import BOM_UTF8, lookup
import re
cookie_re = re.compile("coding[:=]\s*([-\w.]+)")
cookie_re = re.compile(r"coding[:=]\s*([-\w.]+)")
def _get_normal_name(orig_enc):
"""Imitates get_normal_name in tokenizer.c."""
@@ -608,17 +614,20 @@ except ImportError: # pragma: no cover
self.maps[0].clear()
try:
from imp import cache_from_source
except ImportError: # pragma: no cover
def cache_from_source(path, debug_override=None):
assert path.endswith('.py')
if debug_override is None:
debug_override = __debug__
if debug_override:
suffix = 'c'
else:
suffix = 'o'
return path + suffix
from importlib.util import cache_from_source # Python >= 3.4
except ImportError: # pragma: no cover
try:
from imp import cache_from_source
except ImportError: # pragma: no cover
def cache_from_source(path, debug_override=None):
assert path.endswith('.py')
if debug_override is None:
debug_override = __debug__
if debug_override:
suffix = 'c'
else:
suffix = 'o'
return path + suffix
try:
from collections import OrderedDict
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2012-2016 The Python Software Foundation.
# Copyright (C) 2012-2017 The Python Software Foundation.
# See LICENSE.txt and CONTRIBUTORS.txt.
#
"""PEP 376 implementation."""
@@ -257,7 +257,7 @@ class DistributionPath(object):
:type version: string
"""
matcher = None
if not version is None:
if version is not None:
try:
matcher = self._scheme.matcher('%s (%s)' % (name, version))
except ValueError:
@@ -265,18 +265,23 @@ class DistributionPath(object):
(name, version))
for dist in self.get_distributions():
provided = dist.provides
# We hit a problem on Travis where enum34 was installed and doesn't
# have a provides attribute ...
if not hasattr(dist, 'provides'):
logger.debug('No "provides": %s', dist)
else:
provided = dist.provides
for p in provided:
p_name, p_ver = parse_name_and_version(p)
if matcher is None:
if p_name == name:
yield dist
break
else:
if p_name == name and matcher.match(p_ver):
yield dist
break
for p in provided:
p_name, p_ver = parse_name_and_version(p)
if matcher is None:
if p_name == name:
yield dist
break
else:
if p_name == name and matcher.match(p_ver):
yield dist
break
def get_file_path(self, name, relative_path):
"""
@@ -529,9 +534,10 @@ class InstalledDistribution(BaseInstalledDistribution):
hasher = 'sha256'
def __init__(self, path, metadata=None, env=None):
self.modules = []
self.finder = finder = resources.finder_for_path(path)
if finder is None:
import pdb; pdb.set_trace ()
raise ValueError('finder unavailable for %s' % path)
if env and env._cache_enabled and path in env._cache.path:
metadata = env._cache.path[path].metadata
elif metadata is None:
@@ -553,11 +559,13 @@ class InstalledDistribution(BaseInstalledDistribution):
if env and env._cache_enabled:
env._cache.add(self)
try:
r = finder.find('REQUESTED')
except AttributeError:
import pdb; pdb.set_trace ()
r = finder.find('REQUESTED')
self.requested = r is not None
p = os.path.join(path, 'top_level.txt')
if os.path.exists(p):
with open(p, 'rb') as f:
data = f.read()
self.modules = data.splitlines()
def __repr__(self):
return '<InstalledDistribution %r %s at %r>' % (
@@ -917,11 +925,14 @@ class EggInfoDistribution(BaseInstalledDistribution):
pass
return reqs
tl_path = tl_data = None
if path.endswith('.egg'):
if os.path.isdir(path):
meta_path = os.path.join(path, 'EGG-INFO', 'PKG-INFO')
p = os.path.join(path, 'EGG-INFO')
meta_path = os.path.join(p, 'PKG-INFO')
metadata = Metadata(path=meta_path, scheme='legacy')
req_path = os.path.join(path, 'EGG-INFO', 'requires.txt')
req_path = os.path.join(p, 'requires.txt')
tl_path = os.path.join(p, 'top_level.txt')
requires = parse_requires_path(req_path)
else:
# FIXME handle the case where zipfile is not available
@@ -931,6 +942,7 @@ class EggInfoDistribution(BaseInstalledDistribution):
metadata = Metadata(fileobj=fileobj, scheme='legacy')
try:
data = zipf.get_data('EGG-INFO/requires.txt')
tl_data = zipf.get_data('EGG-INFO/top_level.txt').decode('utf-8')
requires = parse_requires_data(data.decode('utf-8'))
except IOError:
requires = None
@@ -939,6 +951,7 @@ class EggInfoDistribution(BaseInstalledDistribution):
req_path = os.path.join(path, 'requires.txt')
requires = parse_requires_path(req_path)
path = os.path.join(path, 'PKG-INFO')
tl_path = os.path.join(path, 'top_level.txt')
metadata = Metadata(path=path, scheme='legacy')
else:
raise DistlibException('path must end with .egg-info or .egg, '
@@ -946,6 +959,16 @@ class EggInfoDistribution(BaseInstalledDistribution):
if requires:
metadata.add_requirements(requires)
# look for top-level modules in top_level.txt, if present
if tl_data is None:
if tl_path is not None and os.path.exists(tl_path):
with open(tl_path, 'rb') as f:
tl_data = f.read().decode('utf-8')
if not tl_data:
tl_data = []
else:
tl_data = tl_data.splitlines()
self.modules = tl_data
return metadata
def __repr__(self):
@@ -1025,20 +1048,21 @@ class EggInfoDistribution(BaseInstalledDistribution):
:returns: iterator of paths
"""
record_path = os.path.join(self.path, 'installed-files.txt')
skip = True
with codecs.open(record_path, 'r', encoding='utf-8') as f:
for line in f:
line = line.strip()
if line == './':
skip = False
continue
if not skip:
p = os.path.normpath(os.path.join(self.path, line))
if p.startswith(self.path):
if absolute:
yield p
else:
yield line
if os.path.exists(record_path):
skip = True
with codecs.open(record_path, 'r', encoding='utf-8') as f:
for line in f:
line = line.strip()
if line == './':
skip = False
continue
if not skip:
p = os.path.normpath(os.path.join(self.path, line))
if p.startswith(self.path):
if absolute:
yield p
else:
yield line
def __eq__(self, other):
return (isinstance(other, EggInfoDistribution) and
@@ -49,7 +49,6 @@ class PackageIndex(object):
self.ssl_verifier = None
self.gpg = None
self.gpg_home = None
self.rpc_proxy = None
with open(os.devnull, 'w') as sink:
# Use gpg by default rather than gpg2, as gpg2 insists on
# prompting for passwords
@@ -510,6 +509,8 @@ class PackageIndex(object):
def search(self, terms, operator=None):
if isinstance(terms, string_types):
terms = {'name': terms}
if self.rpc_proxy is None:
self.rpc_proxy = ServerProxy(self.url, timeout=3.0)
return self.rpc_proxy.search(terms, operator or 'and')
rpc_proxy = ServerProxy(self.url, timeout=3.0)
try:
return rpc_proxy.search(terms, operator or 'and')
finally:
rpc_proxy('close')()
@@ -24,7 +24,7 @@ from .compat import (urljoin, urlparse, urlunparse, url2pathname, pathname2url,
HTTPRedirectHandler as BaseRedirectHandler, text_type,
Request, HTTPError, URLError)
from .database import Distribution, DistributionPath, make_dist
from .metadata import Metadata
from .metadata import Metadata, MetadataInvalidError
from .util import (cached_property, parse_credentials, ensure_slash,
split_filename, get_project_data, parse_requirement,
parse_name_and_version, ServerProxy, normalize_name)
@@ -33,7 +33,7 @@ from .wheel import Wheel, is_compatible
logger = logging.getLogger(__name__)
HASHER_HASH = re.compile('^(\w+)=([a-f0-9]+)')
HASHER_HASH = re.compile(r'^(\w+)=([a-f0-9]+)')
CHARSET = re.compile(r';\s*charset\s*=\s*(.*)\s*$', re.I)
HTML_CONTENT_TYPE = re.compile('text/html|application/x(ht)?ml')
DEFAULT_INDEX = 'https://pypi.org/pypi'
@@ -47,7 +47,10 @@ def get_all_distribution_names(url=None):
if url is None:
url = DEFAULT_INDEX
client = ServerProxy(url, timeout=3.0)
return client.list_packages()
try:
return client.list_packages()
finally:
client('close')()
class RedirectHandler(BaseRedirectHandler):
"""
@@ -66,7 +69,7 @@ class RedirectHandler(BaseRedirectHandler):
if key in headers:
newurl = headers[key]
break
if newurl is None:
if newurl is None: # pragma: no cover
return
urlparts = urlparse(newurl)
if urlparts.scheme == '':
@@ -172,7 +175,7 @@ class Locator(object):
This calls _get_project to do all the work, and just implements a caching layer on top.
"""
if self._cache is None:
if self._cache is None: # pragma: no cover
result = self._get_project(name)
elif name in self._cache:
result = self._cache[name]
@@ -191,10 +194,11 @@ class Locator(object):
basename = posixpath.basename(t.path)
compatible = True
is_wheel = basename.endswith('.whl')
is_downloadable = basename.endswith(self.downloadable_extensions)
if is_wheel:
compatible = is_compatible(Wheel(basename), self.wheel_tags)
return (t.scheme != 'https', 'pypi.org' in t.netloc,
is_wheel, compatible, basename)
return (t.scheme == 'https', 'pypi.org' in t.netloc,
is_downloadable, is_wheel, compatible, basename)
def prefer_url(self, url1, url2):
"""
@@ -237,7 +241,7 @@ class Locator(object):
result = None
scheme, netloc, path, params, query, frag = urlparse(url)
if frag.lower().startswith('egg='):
if frag.lower().startswith('egg='): # pragma: no cover
logger.debug('%s: version hint in fragment: %r',
project_name, frag)
m = HASHER_HASH.match(frag)
@@ -246,7 +250,7 @@ class Locator(object):
else:
algo, digest = None, None
origpath = path
if path and path[-1] == '/':
if path and path[-1] == '/': # pragma: no cover
path = path[:-1]
if path.endswith('.whl'):
try:
@@ -268,13 +272,15 @@ class Locator(object):
}
except Exception as e: # pragma: no cover
logger.warning('invalid path for wheel: %s', path)
elif path.endswith(self.downloadable_extensions):
elif not path.endswith(self.downloadable_extensions): # pragma: no cover
logger.debug('Not downloadable: %s', path)
else: # downloadable extension
path = filename = posixpath.basename(path)
for ext in self.downloadable_extensions:
if path.endswith(ext):
path = path[:-len(ext)]
t = self.split_filename(path, project_name)
if not t:
if not t: # pragma: no cover
logger.debug('No match for project/version: %s', path)
else:
name, version, pyver = t
@@ -287,7 +293,7 @@ class Locator(object):
params, query, '')),
#'packagetype': 'sdist',
}
if pyver:
if pyver: # pragma: no cover
result['python-version'] = pyver
break
if result and algo:
@@ -348,7 +354,7 @@ class Locator(object):
"""
result = None
r = parse_requirement(requirement)
if r is None:
if r is None: # pragma: no cover
raise DistlibException('Not a valid requirement: %r' % requirement)
scheme = get_scheme(self.scheme)
self.matcher = matcher = scheme.matcher(r.requirement)
@@ -386,7 +392,7 @@ class Locator(object):
d = {}
sd = versions.get('digests', {})
for url in result.download_urls:
if url in sd:
if url in sd: # pragma: no cover
d[url] = sd[url]
result.digests = d
self.matcher = None
@@ -520,9 +526,9 @@ class Page(object):
# declared with double quotes, single quotes or no quotes - which leads to
# the length of the expression.
_href = re.compile("""
(rel\s*=\s*(?:"(?P<rel1>[^"]*)"|'(?P<rel2>[^']*)'|(?P<rel3>[^>\s\n]*))\s+)?
href\s*=\s*(?:"(?P<url1>[^"]*)"|'(?P<url2>[^']*)'|(?P<url3>[^>\s\n]*))
(\s+rel\s*=\s*(?:"(?P<rel4>[^"]*)"|'(?P<rel5>[^']*)'|(?P<rel6>[^>\s\n]*)))?
(rel\\s*=\\s*(?:"(?P<rel1>[^"]*)"|'(?P<rel2>[^']*)'|(?P<rel3>[^>\\s\n]*))\\s+)?
href\\s*=\\s*(?:"(?P<url1>[^"]*)"|'(?P<url2>[^']*)'|(?P<url3>[^>\\s\n]*))
(\\s+rel\\s*=\\s*(?:"(?P<rel4>[^"]*)"|'(?P<rel5>[^']*)'|(?P<rel6>[^>\\s\n]*)))?
""", re.I | re.S | re.X)
_base = re.compile(r"""<base\s+href\s*=\s*['"]?([^'">]+)""", re.I | re.S)
@@ -726,11 +732,14 @@ class SimpleScrapingLocator(Locator):
continue
for link, rel in page.links:
if link not in self._seen:
self._seen.add(link)
if (not self._process_download(link) and
self._should_queue(link, url, rel)):
logger.debug('Queueing %s from %s', link, url)
self._to_fetch.put(link)
try:
self._seen.add(link)
if (not self._process_download(link) and
self._should_queue(link, url, rel)):
logger.debug('Queueing %s from %s', link, url)
self._to_fetch.put(link)
except MetadataInvalidError: # e.g. invalid versions
pass
except Exception as e: # pragma: no cover
self.errors.put(text_type(e))
finally:
@@ -1236,7 +1245,7 @@ class DependencyFinder(object):
ireqts = dist.run_requires | dist.meta_requires
sreqts = dist.build_requires
ereqts = set()
if dist in install_dists:
if meta_extras and dist in install_dists:
for key in ('test', 'build', 'dev'):
e = ':%s:' % key
if e in meta_extras:
@@ -24,7 +24,7 @@ __all__ = ['Manifest']
logger = logging.getLogger(__name__)
# a \ followed by some spaces + EOL
_COLLAPSE_PATTERN = re.compile('\\\w*\n', re.M)
_COLLAPSE_PATTERN = re.compile('\\\\w*\n', re.M)
_COMMENTED_LINE = re.compile('#.*?(?=\n)|\n(?=$)', re.M | re.S)
#
+90 -149
View File
@@ -1,182 +1,114 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2012-2013 Vinay Sajip.
# Copyright (C) 2012-2017 Vinay Sajip.
# Licensed to the Python Software Foundation under a contributor agreement.
# See LICENSE.txt and CONTRIBUTORS.txt.
#
"""Parser for the environment markers micro-language defined in PEP 345."""
"""
Parser for the environment markers micro-language defined in PEP 508.
"""
# Note: In PEP 345, the micro-language was Python compatible, so the ast
# module could be used to parse it. However, PEP 508 introduced operators such
# as ~= and === which aren't in Python, necessitating a different approach.
import ast
import os
import sys
import platform
import re
from .compat import python_implementation, string_types
from .util import in_venv
from .compat import python_implementation, urlparse, string_types
from .util import in_venv, parse_marker
__all__ = ['interpret']
def _is_literal(o):
if not isinstance(o, string_types) or not o:
return False
return o[0] in '\'"'
class Evaluator(object):
"""
A limited evaluator for Python expressions.
This class is used to evaluate marker expessions.
"""
operators = {
'eq': lambda x, y: x == y,
'gt': lambda x, y: x > y,
'gte': lambda x, y: x >= y,
operations = {
'==': lambda x, y: x == y,
'===': lambda x, y: x == y,
'~=': lambda x, y: x == y or x > y,
'!=': lambda x, y: x != y,
'<': lambda x, y: x < y,
'<=': lambda x, y: x == y or x < y,
'>': lambda x, y: x > y,
'>=': lambda x, y: x == y or x > y,
'and': lambda x, y: x and y,
'or': lambda x, y: x or y,
'in': lambda x, y: x in y,
'lt': lambda x, y: x < y,
'lte': lambda x, y: x <= y,
'not': lambda x: not x,
'noteq': lambda x, y: x != y,
'notin': lambda x, y: x not in y,
'not in': lambda x, y: x not in y,
}
allowed_values = {
'sys_platform': sys.platform,
'python_version': '%s.%s' % sys.version_info[:2],
# parsing sys.platform is not reliable, but there is no other
# way to get e.g. 2.7.2+, and the PEP is defined with sys.version
'python_full_version': sys.version.split(' ', 1)[0],
'os_name': os.name,
'platform_in_venv': str(in_venv()),
'platform_release': platform.release(),
'platform_version': platform.version(),
'platform_machine': platform.machine(),
'platform_python_implementation': python_implementation(),
}
def __init__(self, context=None):
def evaluate(self, expr, context):
"""
Initialise an instance.
:param context: If specified, names are looked up in this mapping.
Evaluate a marker expression returned by the :func:`parse_requirement`
function in the specified context.
"""
self.context = context or {}
self.source = None
def get_fragment(self, offset):
"""
Get the part of the source which is causing a problem.
"""
fragment_len = 10
s = '%r' % (self.source[offset:offset + fragment_len])
if offset + fragment_len < len(self.source):
s += '...'
return s
def get_handler(self, node_type):
"""
Get a handler for the specified AST node type.
"""
return getattr(self, 'do_%s' % node_type, None)
def evaluate(self, node, filename=None):
"""
Evaluate a source string or node, using ``filename`` when
displaying errors.
"""
if isinstance(node, string_types):
self.source = node
kwargs = {'mode': 'eval'}
if filename:
kwargs['filename'] = filename
try:
node = ast.parse(node, **kwargs)
except SyntaxError as e:
s = self.get_fragment(e.offset)
raise SyntaxError('syntax error %s' % s)
node_type = node.__class__.__name__.lower()
handler = self.get_handler(node_type)
if handler is None:
if self.source is None:
s = '(source not available)'
if isinstance(expr, string_types):
if expr[0] in '\'"':
result = expr[1:-1]
else:
s = self.get_fragment(node.col_offset)
raise SyntaxError("don't know how to evaluate %r %s" % (
node_type, s))
return handler(node)
def get_attr_key(self, node):
assert isinstance(node, ast.Attribute), 'attribute node expected'
return '%s.%s' % (node.value.id, node.attr)
def do_attribute(self, node):
if not isinstance(node.value, ast.Name):
valid = False
if expr not in context:
raise SyntaxError('unknown variable: %s' % expr)
result = context[expr]
else:
key = self.get_attr_key(node)
valid = key in self.context or key in self.allowed_values
if not valid:
raise SyntaxError('invalid expression: %s' % key)
if key in self.context:
result = self.context[key]
else:
result = self.allowed_values[key]
assert isinstance(expr, dict)
op = expr['op']
if op not in self.operations:
raise NotImplementedError('op not implemented: %s' % op)
elhs = expr['lhs']
erhs = expr['rhs']
if _is_literal(expr['lhs']) and _is_literal(expr['rhs']):
raise SyntaxError('invalid comparison: %s %s %s' % (elhs, op, erhs))
lhs = self.evaluate(elhs, context)
rhs = self.evaluate(erhs, context)
result = self.operations[op](lhs, rhs)
return result
def do_boolop(self, node):
result = self.evaluate(node.values[0])
is_or = node.op.__class__ is ast.Or
is_and = node.op.__class__ is ast.And
assert is_or or is_and
if (is_and and result) or (is_or and not result):
for n in node.values[1:]:
result = self.evaluate(n)
if (is_or and result) or (is_and and not result):
break
return result
def default_context():
def format_full_version(info):
version = '%s.%s.%s' % (info.major, info.minor, info.micro)
kind = info.releaselevel
if kind != 'final':
version += kind[0] + str(info.serial)
return version
def do_compare(self, node):
def sanity_check(lhsnode, rhsnode):
valid = True
if isinstance(lhsnode, ast.Str) and isinstance(rhsnode, ast.Str):
valid = False
#elif (isinstance(lhsnode, ast.Attribute)
# and isinstance(rhsnode, ast.Attribute)):
# klhs = self.get_attr_key(lhsnode)
# krhs = self.get_attr_key(rhsnode)
# valid = klhs != krhs
if not valid:
s = self.get_fragment(node.col_offset)
raise SyntaxError('Invalid comparison: %s' % s)
if hasattr(sys, 'implementation'):
implementation_version = format_full_version(sys.implementation.version)
implementation_name = sys.implementation.name
else:
implementation_version = '0'
implementation_name = ''
lhsnode = node.left
lhs = self.evaluate(lhsnode)
result = True
for op, rhsnode in zip(node.ops, node.comparators):
sanity_check(lhsnode, rhsnode)
op = op.__class__.__name__.lower()
if op not in self.operators:
raise SyntaxError('unsupported operation: %r' % op)
rhs = self.evaluate(rhsnode)
result = self.operators[op](lhs, rhs)
if not result:
break
lhs = rhs
lhsnode = rhsnode
return result
result = {
'implementation_name': implementation_name,
'implementation_version': implementation_version,
'os_name': os.name,
'platform_machine': platform.machine(),
'platform_python_implementation': platform.python_implementation(),
'platform_release': platform.release(),
'platform_system': platform.system(),
'platform_version': platform.version(),
'platform_in_venv': str(in_venv()),
'python_full_version': platform.python_version(),
'python_version': platform.python_version()[:3],
'sys_platform': sys.platform,
}
return result
def do_expression(self, node):
return self.evaluate(node.body)
def do_name(self, node):
valid = False
if node.id in self.context:
valid = True
result = self.context[node.id]
elif node.id in self.allowed_values:
valid = True
result = self.allowed_values[node.id]
if not valid:
raise SyntaxError('invalid expression: %s' % node.id)
return result
def do_str(self, node):
return node.s
DEFAULT_CONTEXT = default_context()
del default_context
evaluator = Evaluator()
def interpret(marker, execution_context=None):
"""
@@ -187,4 +119,13 @@ def interpret(marker, execution_context=None):
:param execution_context: The context used for name lookup.
:type execution_context: mapping
"""
return Evaluator(execution_context).evaluate(marker.strip())
try:
expr, rest = parse_marker(marker)
except Exception as e:
raise SyntaxError('Unable to interpret marker syntax: %s: %s' % (marker, e))
if rest and rest[0] != '#':
raise SyntaxError('unexpected trailing data in marker: %s: %s' % (marker, rest))
context = dict(DEFAULT_CONTEXT)
if execution_context:
context.update(execution_context)
return evaluator.evaluate(expr, context)
@@ -50,7 +50,7 @@ PKG_INFO_ENCODING = 'utf-8'
# to 1.2 once PEP 345 is supported everywhere
PKG_INFO_PREFERRED_VERSION = '1.1'
_LINE_PREFIX_1_2 = re.compile('\n \|')
_LINE_PREFIX_1_2 = re.compile('\n \\|')
_LINE_PREFIX_PRE_1_2 = re.compile('\n ')
_241_FIELDS = ('Metadata-Version', 'Name', 'Version', 'Platform',
'Summary', 'Description',
@@ -91,11 +91,16 @@ _426_FIELDS = ('Metadata-Version', 'Name', 'Version', 'Platform',
_426_MARKERS = ('Private-Version', 'Provides-Extra', 'Obsoleted-By',
'Setup-Requires-Dist', 'Extension')
_566_FIELDS = _426_FIELDS + ('Description-Content-Type',)
_566_MARKERS = ('Description-Content-Type',)
_ALL_FIELDS = set()
_ALL_FIELDS.update(_241_FIELDS)
_ALL_FIELDS.update(_314_FIELDS)
_ALL_FIELDS.update(_345_FIELDS)
_ALL_FIELDS.update(_426_FIELDS)
_ALL_FIELDS.update(_566_FIELDS)
EXTRA_RE = re.compile(r'''extra\s*==\s*("([^"]+)"|'([^']+)')''')
@@ -107,6 +112,8 @@ def _version2fieldlist(version):
return _314_FIELDS
elif version == '1.2':
return _345_FIELDS
elif version in ('1.3', '2.1'):
return _345_FIELDS + _566_FIELDS
elif version == '2.0':
return _426_FIELDS
raise MetadataUnrecognizedVersionError(version)
@@ -126,38 +133,51 @@ def _best_version(fields):
continue
keys.append(key)
possible_versions = ['1.0', '1.1', '1.2', '2.0']
possible_versions = ['1.0', '1.1', '1.2', '1.3', '2.0', '2.1']
# first let's try to see if a field is not part of one of the version
for key in keys:
if key not in _241_FIELDS and '1.0' in possible_versions:
possible_versions.remove('1.0')
logger.debug('Removed 1.0 due to %s', key)
if key not in _314_FIELDS and '1.1' in possible_versions:
possible_versions.remove('1.1')
logger.debug('Removed 1.1 due to %s', key)
if key not in _345_FIELDS and '1.2' in possible_versions:
possible_versions.remove('1.2')
logger.debug('Removed 1.2 due to %s', key)
if key not in _566_FIELDS and '1.3' in possible_versions:
possible_versions.remove('1.3')
logger.debug('Removed 1.3 due to %s', key)
if key not in _566_FIELDS and '2.1' in possible_versions:
if key != 'Description': # In 2.1, description allowed after headers
possible_versions.remove('2.1')
logger.debug('Removed 2.1 due to %s', key)
if key not in _426_FIELDS and '2.0' in possible_versions:
possible_versions.remove('2.0')
logger.debug('Removed 2.0 due to %s', key)
# possible_version contains qualified versions
if len(possible_versions) == 1:
return possible_versions[0] # found !
elif len(possible_versions) == 0:
logger.debug('Out of options - unknown metadata set: %s', fields)
raise MetadataConflictError('Unknown metadata set')
# let's see if one unique marker is found
is_1_1 = '1.1' in possible_versions and _has_marker(keys, _314_MARKERS)
is_1_2 = '1.2' in possible_versions and _has_marker(keys, _345_MARKERS)
is_2_1 = '2.1' in possible_versions and _has_marker(keys, _566_MARKERS)
is_2_0 = '2.0' in possible_versions and _has_marker(keys, _426_MARKERS)
if int(is_1_1) + int(is_1_2) + int(is_2_0) > 1:
raise MetadataConflictError('You used incompatible 1.1/1.2/2.0 fields')
if int(is_1_1) + int(is_1_2) + int(is_2_1) + int(is_2_0) > 1:
raise MetadataConflictError('You used incompatible 1.1/1.2/2.0/2.1 fields')
# we have the choice, 1.0, or 1.2, or 2.0
# - 1.0 has a broken Summary field but works with all tools
# - 1.1 is to avoid
# - 1.2 fixes Summary but has little adoption
# - 2.0 adds more features and is very new
if not is_1_1 and not is_1_2 and not is_2_0:
if not is_1_1 and not is_1_2 and not is_2_1 and not is_2_0:
# we couldn't find any specific marker
if PKG_INFO_PREFERRED_VERSION in possible_versions:
return PKG_INFO_PREFERRED_VERSION
@@ -165,6 +185,8 @@ def _best_version(fields):
return '1.1'
if is_1_2:
return '1.2'
if is_2_1:
return '2.1'
return '2.0'
@@ -355,6 +377,7 @@ class LegacyMetadata(object):
value = msg[field]
if value is not None and value != 'UNKNOWN':
self.set(field, value)
logger.debug('Attempting to set metadata for %s', self)
self.set_metadata_version()
def write(self, filepath, skip_unknown=False):
@@ -634,7 +657,7 @@ class Metadata(object):
instance which handles the key-value metadata format.
"""
METADATA_VERSION_MATCHER = re.compile('^\d+(\.\d+)*$')
METADATA_VERSION_MATCHER = re.compile(r'^\d+(\.\d+)*$')
NAME_MATCHER = re.compile('^[0-9A-Z]([0-9A-Z_.-]*[0-9A-Z])?$', re.I)
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013-2016 Vinay Sajip.
# Copyright (C) 2013-2017 Vinay Sajip.
# Licensed to the Python Software Foundation under a contributor agreement.
# See LICENSE.txt and CONTRIBUTORS.txt.
#
@@ -38,7 +38,7 @@ _DEFAULT_MANIFEST = '''
# check if Python is called on the first line with this expression
FIRST_LINE_RE = re.compile(b'^#!.*pythonw?[0-9.]*([ \t].*)?$')
SCRIPT_TEMPLATE = '''# -*- coding: utf-8 -*-
SCRIPT_TEMPLATE = r'''# -*- coding: utf-8 -*-
if __name__ == '__main__':
import sys, re
@@ -57,7 +57,7 @@ if __name__ == '__main__':
func = _resolve('%(module)s', '%(func)s')
rc = func() # None interpreted as 0
except Exception as e: # only supporting Python >= 2.6
sys.stderr.write('%%s\\n' %% e)
sys.stderr.write('%%s\n' %% e)
rc = 1
sys.exit(rc)
'''
@@ -136,6 +136,37 @@ class ScriptMaker(object):
return executable
return '/usr/bin/env %s' % executable
def _build_shebang(self, executable, post_interp):
"""
Build a shebang line. In the simple case (on Windows, or a shebang line
which is not too long or contains spaces) use a simple formulation for
the shebang. Otherwise, use /bin/sh as the executable, with a contrived
shebang which allows the script to run either under Python or sh, using
suitable quoting. Thanks to Harald Nordgren for his input.
See also: http://www.in-ulm.de/~mascheck/various/shebang/#length
https://hg.mozilla.org/mozilla-central/file/tip/mach
"""
if os.name != 'posix':
simple_shebang = True
else:
# Add 3 for '#!' prefix and newline suffix.
shebang_length = len(executable) + len(post_interp) + 3
if sys.platform == 'darwin':
max_shebang_length = 512
else:
max_shebang_length = 127
simple_shebang = ((b' ' not in executable) and
(shebang_length <= max_shebang_length))
if simple_shebang:
result = b'#!' + executable + post_interp + b'\n'
else:
result = b'#!/bin/sh\n'
result += b"'''exec' " + executable + post_interp + b' "$0" "$@"\n'
result += b"' '''"
return result
def _get_shebang(self, encoding, post_interp=b'', options=None):
enquote = True
if self.executable:
@@ -169,7 +200,7 @@ class ScriptMaker(object):
if (sys.platform == 'cli' and '-X:Frames' not in post_interp
and '-X:FullFrames' not in post_interp): # pragma: no cover
post_interp += b' -X:Frames'
shebang = b'#!' + executable + post_interp + b'\n'
shebang = self._build_shebang(executable, post_interp)
# Python parser starts to read a script using UTF-8 until
# it gets a #coding:xxx cookie. The shebang has to be the
# first line of a file, the #coding:xxx cookie cannot be
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+225 -81
View File
@@ -1,5 +1,5 @@
#
# Copyright (C) 2012-2016 The Python Software Foundation.
# Copyright (C) 2012-2017 The Python Software Foundation.
# See LICENSE.txt and CONTRIBUTORS.txt.
#
import codecs
@@ -13,7 +13,6 @@ import logging
import os
import py_compile
import re
import shutil
import socket
try:
import ssl
@@ -36,101 +35,243 @@ from .compat import (string_types, text_type, shutil, raw_input, StringIO,
cache_from_source, urlopen, urljoin, httplib, xmlrpclib,
splittype, HTTPHandler, BaseConfigurator, valid_ident,
Container, configparser, URLError, ZipFile, fsdecode,
unquote)
unquote, urlparse)
logger = logging.getLogger(__name__)
#
# Requirement parsing code for name + optional constraints + optional extras
#
# e.g. 'foo >= 1.2, < 2.0 [bar, baz]'
#
# The regex can seem a bit hairy, so we build it up out of smaller pieces
# which are manageable.
# Requirement parsing code as per PEP 508
#
COMMA = r'\s*,\s*'
COMMA_RE = re.compile(COMMA)
IDENTIFIER = re.compile(r'^([\w\.-]+)\s*')
VERSION_IDENTIFIER = re.compile(r'^([\w\.*+-]+)\s*')
COMPARE_OP = re.compile(r'^(<=?|>=?|={2,3}|[~!]=)\s*')
MARKER_OP = re.compile(r'^((<=?)|(>=?)|={2,3}|[~!]=|in|not\s+in)\s*')
OR = re.compile(r'^or\b\s*')
AND = re.compile(r'^and\b\s*')
NON_SPACE = re.compile(r'(\S+)\s*')
STRING_CHUNK = re.compile(r'([\s\w\.{}()*+#:;,/?!~`@$%^&=|<>\[\]-]+)')
IDENT = r'(\w|[.-])+'
EXTRA_IDENT = r'(\*|:(\*|\w+):|' + IDENT + ')'
VERSPEC = IDENT + r'\*?'
RELOP = '([<>=!~]=)|[<>]'
def parse_marker(marker_string):
"""
Parse a marker string and return a dictionary containing a marker expression.
#
# The first relop is optional - if absent, will be taken as '~='
#
BARE_CONSTRAINTS = ('(' + RELOP + r')?\s*(' + VERSPEC + ')(' + COMMA + '(' +
RELOP + r')\s*(' + VERSPEC + '))*')
DIRECT_REF = '(from\s+(?P<diref>.*))'
#
# Either the bare constraints or the bare constraints in parentheses
#
CONSTRAINTS = (r'\(\s*(?P<c1>' + BARE_CONSTRAINTS + '|' + DIRECT_REF +
r')\s*\)|(?P<c2>' + BARE_CONSTRAINTS + '\s*)')
EXTRA_LIST = EXTRA_IDENT + '(' + COMMA + EXTRA_IDENT + ')*'
EXTRAS = r'\[\s*(?P<ex>' + EXTRA_LIST + r')?\s*\]'
REQUIREMENT = ('(?P<dn>' + IDENT + r')\s*(' + EXTRAS + r'\s*)?(\s*' +
CONSTRAINTS + ')?$')
REQUIREMENT_RE = re.compile(REQUIREMENT)
#
# Used to scan through the constraints
#
RELOP_IDENT = '(?P<op>' + RELOP + r')\s*(?P<vn>' + VERSPEC + ')'
RELOP_IDENT_RE = re.compile(RELOP_IDENT)
def parse_requirement(s):
def get_constraint(m):
d = m.groupdict()
return d['op'], d['vn']
result = None
m = REQUIREMENT_RE.match(s)
if m:
d = m.groupdict()
name = d['dn']
cons = d['c1'] or d['c2']
if not d['diref']:
url = None
The dictionary will contain keys "op", "lhs" and "rhs" for non-terminals in
the expression grammar, or strings. A string contained in quotes is to be
interpreted as a literal string, and a string not contained in quotes is a
variable (such as os_name).
"""
def marker_var(remaining):
# either identifier, or literal string
m = IDENTIFIER.match(remaining)
if m:
result = m.groups()[0]
remaining = remaining[m.end():]
elif not remaining:
raise SyntaxError('unexpected end of input')
else:
# direct reference
cons = None
url = d['diref'].strip()
if not cons:
cons = None
constr = ''
rs = d['dn']
q = remaining[0]
if q not in '\'"':
raise SyntaxError('invalid expression: %s' % remaining)
oq = '\'"'.replace(q, '')
remaining = remaining[1:]
parts = [q]
while remaining:
# either a string chunk, or oq, or q to terminate
if remaining[0] == q:
break
elif remaining[0] == oq:
parts.append(oq)
remaining = remaining[1:]
else:
m = STRING_CHUNK.match(remaining)
if not m:
raise SyntaxError('error in string literal: %s' % remaining)
parts.append(m.groups()[0])
remaining = remaining[m.end():]
else:
s = ''.join(parts)
raise SyntaxError('unterminated string: %s' % s)
parts.append(q)
result = ''.join(parts)
remaining = remaining[1:].lstrip() # skip past closing quote
return result, remaining
def marker_expr(remaining):
if remaining and remaining[0] == '(':
result, remaining = marker(remaining[1:].lstrip())
if remaining[0] != ')':
raise SyntaxError('unterminated parenthesis: %s' % remaining)
remaining = remaining[1:].lstrip()
else:
if cons[0] not in '<>!=':
cons = '~=' + cons
iterator = RELOP_IDENT_RE.finditer(cons)
cons = [get_constraint(m) for m in iterator]
rs = '%s (%s)' % (name, ', '.join(['%s %s' % con for con in cons]))
if not d['ex']:
lhs, remaining = marker_var(remaining)
while remaining:
m = MARKER_OP.match(remaining)
if not m:
break
op = m.groups()[0]
remaining = remaining[m.end():]
rhs, remaining = marker_var(remaining)
lhs = {'op': op, 'lhs': lhs, 'rhs': rhs}
result = lhs
return result, remaining
def marker_and(remaining):
lhs, remaining = marker_expr(remaining)
while remaining:
m = AND.match(remaining)
if not m:
break
remaining = remaining[m.end():]
rhs, remaining = marker_expr(remaining)
lhs = {'op': 'and', 'lhs': lhs, 'rhs': rhs}
return lhs, remaining
def marker(remaining):
lhs, remaining = marker_and(remaining)
while remaining:
m = OR.match(remaining)
if not m:
break
remaining = remaining[m.end():]
rhs, remaining = marker_and(remaining)
lhs = {'op': 'or', 'lhs': lhs, 'rhs': rhs}
return lhs, remaining
return marker(marker_string)
def parse_requirement(req):
"""
Parse a requirement passed in as a string. Return a Container
whose attributes contain the various parts of the requirement.
"""
remaining = req.strip()
if not remaining or remaining.startswith('#'):
return None
m = IDENTIFIER.match(remaining)
if not m:
raise SyntaxError('name expected: %s' % remaining)
distname = m.groups()[0]
remaining = remaining[m.end():]
extras = mark_expr = versions = uri = None
if remaining and remaining[0] == '[':
i = remaining.find(']', 1)
if i < 0:
raise SyntaxError('unterminated extra: %s' % remaining)
s = remaining[1:i]
remaining = remaining[i + 1:].lstrip()
extras = []
while s:
m = IDENTIFIER.match(s)
if not m:
raise SyntaxError('malformed extra: %s' % s)
extras.append(m.groups()[0])
s = s[m.end():]
if not s:
break
if s[0] != ',':
raise SyntaxError('comma expected in extras: %s' % s)
s = s[1:].lstrip()
if not extras:
extras = None
if remaining:
if remaining[0] == '@':
# it's a URI
remaining = remaining[1:].lstrip()
m = NON_SPACE.match(remaining)
if not m:
raise SyntaxError('invalid URI: %s' % remaining)
uri = m.groups()[0]
t = urlparse(uri)
# there are issues with Python and URL parsing, so this test
# is a bit crude. See bpo-20271, bpo-23505. Python doesn't
# always parse invalid URLs correctly - it should raise
# exceptions for malformed URLs
if not (t.scheme and t.netloc):
raise SyntaxError('Invalid URL: %s' % uri)
remaining = remaining[m.end():].lstrip()
else:
extras = COMMA_RE.split(d['ex'])
result = Container(name=name, constraints=cons, extras=extras,
requirement=rs, source=s, url=url)
return result
def get_versions(ver_remaining):
"""
Return a list of operator, version tuples if any are
specified, else None.
"""
m = COMPARE_OP.match(ver_remaining)
versions = None
if m:
versions = []
while True:
op = m.groups()[0]
ver_remaining = ver_remaining[m.end():]
m = VERSION_IDENTIFIER.match(ver_remaining)
if not m:
raise SyntaxError('invalid version: %s' % ver_remaining)
v = m.groups()[0]
versions.append((op, v))
ver_remaining = ver_remaining[m.end():]
if not ver_remaining or ver_remaining[0] != ',':
break
ver_remaining = ver_remaining[1:].lstrip()
m = COMPARE_OP.match(ver_remaining)
if not m:
raise SyntaxError('invalid constraint: %s' % ver_remaining)
if not versions:
versions = None
return versions, ver_remaining
if remaining[0] != '(':
versions, remaining = get_versions(remaining)
else:
i = remaining.find(')', 1)
if i < 0:
raise SyntaxError('unterminated parenthesis: %s' % remaining)
s = remaining[1:i]
remaining = remaining[i + 1:].lstrip()
# As a special diversion from PEP 508, allow a version number
# a.b.c in parentheses as a synonym for ~= a.b.c (because this
# is allowed in earlier PEPs)
if COMPARE_OP.match(s):
versions, _ = get_versions(s)
else:
m = VERSION_IDENTIFIER.match(s)
if not m:
raise SyntaxError('invalid constraint: %s' % s)
v = m.groups()[0]
s = s[m.end():].lstrip()
if s:
raise SyntaxError('invalid constraint: %s' % s)
versions = [('~=', v)]
if remaining:
if remaining[0] != ';':
raise SyntaxError('invalid requirement: %s' % remaining)
remaining = remaining[1:].lstrip()
mark_expr, remaining = parse_marker(remaining)
if remaining and remaining[0] != '#':
raise SyntaxError('unexpected trailing data: %s' % remaining)
if not versions:
rs = distname
else:
rs = '%s %s' % (distname, ', '.join(['%s %s' % con for con in versions]))
return Container(name=distname, extras=extras, constraints=versions,
marker=mark_expr, url=uri, requirement=rs)
def get_resources_dests(resources_root, rules):
"""Find destinations for resources files"""
def get_rel_path(base, path):
def get_rel_path(root, path):
# normalizes and returns a lstripped-/-separated path
base = base.replace(os.path.sep, '/')
root = root.replace(os.path.sep, '/')
path = path.replace(os.path.sep, '/')
assert path.startswith(base)
return path[len(base):].lstrip('/')
assert path.startswith(root)
return path[len(root):].lstrip('/')
destinations = {}
for base, suffix, dest in rules:
@@ -161,7 +302,7 @@ def in_venv():
def get_executable():
# The __PYVENV_LAUNCHER__ dance is apparently no longer needed, as
# changes to the stub launcher mean that sys.executable always points
# to the stub on macOS
# to the stub on OS X
# if sys.platform == 'darwin' and ('__PYVENV_LAUNCHER__'
# in os.environ):
# result = os.environ['__PYVENV_LAUNCHER__']
@@ -1443,6 +1584,9 @@ def _csv_open(fn, mode, **kwargs):
mode += 'b'
else:
kwargs['newline'] = ''
# Python 3 determines encoding from locale. Force 'utf-8'
# file encoding to match other forced utf-8 encoding
kwargs['encoding'] = 'utf-8'
return open(fn, mode, **kwargs)
@@ -1558,11 +1702,11 @@ class Configurator(BaseConfigurator):
result = json.load(f)
return result
#
# Mixin for running subprocesses and capturing their output
#
class SubprocessMixin(object):
"""
Mixin for running subprocesses and capturing their output
"""
def __init__(self, verbose=False, progress=None):
self.verbose = verbose
self.progress = progress
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2012-2016 The Python Software Foundation.
# Copyright (C) 2012-2017 The Python Software Foundation.
# See LICENSE.txt and CONTRIBUTORS.txt.
#
"""
@@ -12,6 +12,7 @@ import logging
import re
from .compat import string_types
from .util import parse_requirement
__all__ = ['NormalizedVersion', 'NormalizedMatcher',
'LegacyVersion', 'LegacyMatcher',
@@ -78,10 +79,6 @@ class Version(object):
class Matcher(object):
version_class = None
dist_re = re.compile(r"^(\w[\s\w'.-]*)(\((.*)\))?")
comp_re = re.compile(r'^(<=|>=|<|>|!=|={2,3}|~=)?\s*([^\s,]+)$')
num_re = re.compile(r'^\d+(\.\d+)*$')
# value is either a callable or the name of a method
_operators = {
'<': lambda v, c, p: v < c,
@@ -95,26 +92,24 @@ class Matcher(object):
'!=': lambda v, c, p: v != c,
}
# this is a method only to support alternative implementations
# via overriding
def parse_requirement(self, s):
return parse_requirement(s)
def __init__(self, s):
if self.version_class is None:
raise ValueError('Please specify a version class')
self._string = s = s.strip()
m = self.dist_re.match(s)
if not m:
r = self.parse_requirement(s)
if not r:
raise ValueError('Not valid: %r' % s)
groups = m.groups('')
self.name = groups[0].strip()
self.name = r.name
self.key = self.name.lower() # for case-insensitive comparisons
clist = []
if groups[2]:
constraints = [c.strip() for c in groups[2].split(',')]
for c in constraints:
m = self.comp_re.match(c)
if not m:
raise ValueError('Invalid %r in %r' % (c, s))
groups = m.groups()
op = groups[0] or '~='
s = groups[1]
if r.constraints:
# import pdb; pdb.set_trace()
for op, s in r.constraints:
if s.endswith('.*'):
if op not in ('==', '!='):
raise ValueError('\'.*\' not allowed for '
@@ -122,9 +117,8 @@ class Matcher(object):
# Could be a partial version (e.g. for '2.*') which
# won't parse as a version, so keep it as a string
vn, prefix = s[:-2], True
if not self.num_re.match(vn):
# Just to check that vn is a valid version
self.version_class(vn)
# Just to check that vn is a valid version
self.version_class(vn)
else:
# Should parse as a version, so we can create an
# instance for the comparison
@@ -400,7 +394,7 @@ _REPLACEMENTS = (
_SUFFIX_REPLACEMENTS = (
(re.compile('^[:~._+-]+'), ''), # remove leading puncts
(re.compile('[,*")([\]]'), ''), # remove unwanted chars
(re.compile('[,*")([\\]]'), ''), # remove unwanted chars
(re.compile('[~:+_ -]'), '.'), # replace illegal chars
(re.compile('[.]{2,}'), '.'), # multiple runs of '.'
(re.compile(r'\.$'), ''), # trailing '.'
@@ -628,7 +622,7 @@ class LegacyMatcher(Matcher):
_operators = dict(Matcher._operators)
_operators['~='] = '_match_compatible'
numeric_re = re.compile('^(\d+(\.\d+)*)')
numeric_re = re.compile(r'^(\d+(\.\d+)*)')
def _match_compatible(self, version, constraint, prefix):
if version < constraint:
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+22 -16
View File
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013-2016 Vinay Sajip.
# Copyright (C) 2013-2017 Vinay Sajip.
# Licensed to the Python Software Foundation under a contributor agreement.
# See LICENSE.txt and CONTRIBUTORS.txt.
#
@@ -26,7 +26,7 @@ import zipfile
from . import __version__, DistlibException
from .compat import sysconfig, ZipFile, fsdecode, text_type, filter
from .database import InstalledDistribution
from .metadata import Metadata, METADATA_FILENAME
from .metadata import Metadata, METADATA_FILENAME, WHEEL_METADATA_FILENAME
from .util import (FileOperator, convert_path, CSVReader, CSVWriter, Cache,
cached_property, get_cache_base, read_exports, tempdir)
from .version import NormalizedVersion, UnsupportedVersionError
@@ -35,11 +35,11 @@ logger = logging.getLogger(__name__)
cache = None # created when needed
if hasattr(sys, 'pypy_version_info'):
if hasattr(sys, 'pypy_version_info'): # pragma: no cover
IMP_PREFIX = 'pp'
elif sys.platform.startswith('java'):
elif sys.platform.startswith('java'): # pragma: no cover
IMP_PREFIX = 'jy'
elif sys.platform == 'cli':
elif sys.platform == 'cli': # pragma: no cover
IMP_PREFIX = 'ip'
else:
IMP_PREFIX = 'cp'
@@ -222,17 +222,23 @@ class Wheel(object):
wv = wheel_metadata['Wheel-Version'].split('.', 1)
file_version = tuple([int(i) for i in wv])
if file_version < (1, 1):
fn = 'METADATA'
fns = [WHEEL_METADATA_FILENAME, METADATA_FILENAME, 'METADATA']
else:
fn = METADATA_FILENAME
try:
metadata_filename = posixpath.join(info_dir, fn)
with zf.open(metadata_filename) as bf:
wf = wrapper(bf)
result = Metadata(fileobj=wf)
except KeyError:
raise ValueError('Invalid wheel, because %s is '
'missing' % fn)
fns = [WHEEL_METADATA_FILENAME, METADATA_FILENAME]
result = None
for fn in fns:
try:
metadata_filename = posixpath.join(info_dir, fn)
with zf.open(metadata_filename) as bf:
wf = wrapper(bf)
result = Metadata(fileobj=wf)
if result:
break
except KeyError:
pass
if not result:
raise ValueError('Invalid wheel, because metadata is '
'missing: looked in %s' % ', '.join(fns))
return result
def get_wheel_metadata(self, zf):
@@ -919,7 +925,7 @@ def compatible_tags():
arches = [ARCH]
if sys.platform == 'darwin':
m = re.match('(\w+)_(\d+)_(\d+)_(\w+)$', ARCH)
m = re.match(r'(\w+)_(\d+)_(\d+)_(\w+)$', ARCH)
if m:
name, major, minor, arch = m.groups()
minor = int(minor)

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