Merge pull request #4545 from pradyunsg/mypy/infrastructure

This commit is contained in:
Donald Stufft 2017-09-04 02:14:25 -04:00 committed by GitHub
commit 28cca11e28
42 changed files with 316 additions and 106 deletions

1
.gitignore vendored
View File

@ -5,6 +5,7 @@ pip.egg-info/
MANIFEST
.tox
.cache
.mypy_cache
*.egg
*.eggs
*.py[cod]

View File

@ -7,6 +7,7 @@ matrix:
- env: TOXENV=docs
- env: TOXENV=lint-py2
- env: TOXENV=lint-py3
- env: TOXENV=mypy
- env: TOXENV=packaging
# PyPy jobs start first -- they are the slowest
- env: TOXENV=pypy

View File

@ -18,6 +18,9 @@ exclude appveyor.yml
recursive-include src/pip/_vendor *.pem
recursive-include docs Makefile *.rst *.py *.bat
exclude src/pip/_vendor/six
recursive-exclude src/pip/_vendor *.pyi
prune .github
prune .travis
prune docs/_build

1
news/4545.feature Normal file
View File

@ -0,0 +1 @@
Integrate with mypy for utilizing static typing.

View File

@ -11,9 +11,19 @@ known_first_party =
default_section = THIRDPARTY
[flake8]
# Ignoring unused imports since mypy would warn of that.
ignore = F401
exclude = .tox,.idea,*.egg,build,_vendor,data
select = E,W,F
[mypy]
follow_imports = silent
ignore_missing_imports = True
[mypy-pip/_vendor/*]
follow_imports = skip
ignore_errors = True
[tool:pytest]
addopts = --ignore src/pip/_vendor --ignore tests/tests_cache

View File

@ -55,15 +55,6 @@ from pip._vendor.requests.packages.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 pip import cmdoptions" is recursive
# and appears not to work properly in that situation.
# import pip._internal.cmdoptions
# cmdoptions = pip._internal.cmdoptions
logger = logging.getLogger(__name__)
# Hide the InsecureRequestWarning from urllib3
@ -241,7 +232,7 @@ def main(args=None):
sys.exit(1)
# Needed for locale.getpreferredencoding(False) to work
# in pip.utils.encoding.auto_decode
# in pip._internal.utils.encoding.auto_decode
try:
locale.setlocale(locale.LC_ALL, '')
except locale.Error as e:

View File

@ -20,7 +20,8 @@ from pip._internal.exceptions import (
)
from pip._internal.index import PackageFinder
from pip._internal.locations import running_under_virtualenv
from pip._internal.req import InstallRequirement, parse_requirements
from pip._internal.req.req_file import parse_requirements
from pip._internal.req.req_install import InstallRequirement
from pip._internal.status_codes import (
ERROR, PREVIOUS_BUILD_DIR_ERROR, SUCCESS, UNKNOWN_ERROR,
VIRTUALENV_NOT_FOUND
@ -29,18 +30,21 @@ from pip._internal.utils import deprecation
from pip._internal.utils.logging import IndentingFormatter
from pip._internal.utils.misc import get_prog, normalize_path
from pip._internal.utils.outdated import pip_version_check
from pip._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
ignore_require_venv = 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):

View File

@ -19,8 +19,12 @@ from pip._internal.index import (
from pip._internal.locations import USER_CACHE_DIR, src_prefix
from pip._internal.models import PyPI
from pip._internal.utils.hashes import STRONG_HASHES
from pip._internal.utils.typing import MYPY_CHECK_RUNNING
from pip._internal.utils.ui import BAR_TYPES
if MYPY_CHECK_RUNNING:
from typing import Any
def make_option_group(group, parser):
"""
@ -64,7 +68,8 @@ help_ = partial(
'-h', '--help',
dest='help',
action='help',
help='Show help.')
help='Show help.',
) # type: Any
isolated_mode = partial(
Option,
@ -85,7 +90,8 @@ require_virtualenv = partial(
dest='require_venv',
action='store_true',
default=False,
help=SUPPRESS_HELP)
help=SUPPRESS_HELP
) # type: Any
verbose = partial(
Option,
@ -101,7 +107,8 @@ version = partial(
'-V', '--version',
dest='version',
action='store_true',
help='Show version and exit.')
help='Show version and exit.',
) # type: Any
quiet = partial(
Option,
@ -109,10 +116,12 @@ 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,
@ -121,8 +130,11 @@ progress_bar = partial(
type='choice',
choices=list(BAR_TYPES.keys()),
default='on',
help='Specify type of progress to be displayed [' +
'|'.join(BAR_TYPES.keys()) + '] (default: %default)')
help=(
'Specify type of progress to be displayed [' +
'|'.join(BAR_TYPES.keys()) + '] (default: %default)'
),
) # type: Any
log = partial(
Option,
@ -130,7 +142,7 @@ log = partial(
dest="log",
metavar="path",
help="Path to a verbose appending log."
)
) # type: Any
no_input = partial(
Option,
@ -139,7 +151,8 @@ no_input = partial(
dest='no_input',
action='store_true',
default=False,
help=SUPPRESS_HELP)
help=SUPPRESS_HELP
) # type: Any
proxy = partial(
Option,
@ -147,7 +160,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,
@ -156,7 +170,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,
@ -165,7 +180,8 @@ timeout = partial(
dest='timeout',
type='float',
default=15,
help='Set the socket timeout (default %default seconds).')
help='Set the socket timeout (default %default seconds).',
) # type: Any
skip_requirements_regex = partial(
Option,
@ -174,7 +190,8 @@ skip_requirements_regex = partial(
dest='skip_requirements_regex',
type='str',
default='',
help=SUPPRESS_HELP)
help=SUPPRESS_HELP,
) # type: Any
def exists_action():
@ -188,7 +205,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(
@ -197,7 +215,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,
@ -207,7 +226,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,
@ -218,7 +238,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():
@ -230,7 +251,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.",
)
@ -240,7 +261,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():
@ -252,7 +274,8 @@ 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.")
"then look for archives in the directory listing.",
)
def trusted_host():
@ -275,7 +298,7 @@ process_dependency_links = partial(
action="store_true",
default=False,
help="Enable the processing of dependency links.",
)
) # type: Any
def constraints():
@ -286,7 +309,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():
@ -297,7 +321,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():
@ -321,7 +346,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".'
)
) # type: Any
def _get_format_control(values, option):
@ -351,7 +376,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():
@ -364,7 +390,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(
@ -390,7 +417,8 @@ 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,
@ -398,14 +426,15 @@ build_dir = partial(
dest='build_dir',
metavar='dir',
help='Directory to unpack packages into and build in.'
)
) # 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
install_options = partial(
Option,
@ -417,7 +446,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,
@ -426,14 +456,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,
@ -441,7 +473,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,
@ -450,7 +483,8 @@ 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
@ -460,7 +494,7 @@ always_unzip = partial(
dest='always_unzip',
action='store_true',
help=SUPPRESS_HELP,
)
) # type: Any
def _merge_hash(option, opt_str, value, parser):
@ -490,7 +524,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(
@ -501,7 +536,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
##########

View File

@ -17,6 +17,12 @@ from pip._internal.commands.install import InstallCommand
from pip._internal.commands.uninstall import UninstallCommand
from pip._internal.commands.wheel import WheelCommand
from pip._internal.utils.typing import MYPY_CHECK_RUNNING
if MYPY_CHECK_RUNNING:
from typing import List, Type
from pip._internal.basecommand import Command
commands_order = [
InstallCommand,
DownloadCommand,
@ -31,7 +37,7 @@ commands_order = [
HashCommand,
CompletionCommand,
HelpCommand,
]
] # type: List[Type[Command]]
commands_dict = {c.name: c for c in commands_order}

View File

@ -5,6 +5,7 @@ import logging
import warnings
from pip._vendor import six
from pip._vendor.six.moves import zip_longest
from pip._internal.basecommand import Command
from pip._internal.cmdoptions import index_group, make_option_group
@ -16,12 +17,6 @@ from pip._internal.utils.misc import (
)
from pip._internal.utils.packaging import get_installer
try:
from itertools import zip_longest
except ImportError:
from itertools import izip_longest as zip_longest
logger = logging.getLogger(__name__)

View File

@ -7,7 +7,9 @@ from collections import OrderedDict
from pip._vendor import pkg_resources
from pip._vendor.packaging.version import parse as parse_version
from pip._vendor.six.moves import xmlrpc_client
# NOTE: XMLRPC Client is not annotated in typeshed as on 2017-07-17, which is
# why we ignore the type on this import
from pip._vendor.six.moves import xmlrpc_client # type: ignore
from pip._internal.basecommand import SUCCESS, Command
from pip._internal.download import PipXmlrpcTransport

View File

@ -14,9 +14,9 @@ try:
import ipaddress
except ImportError:
try:
from pip._vendor import ipaddress
from pip._vendor import ipaddress # type: ignore
except ImportError:
import ipaddr as ipaddress
import ipaddr as ipaddress # type: ignore
ipaddress.ip_address = ipaddress.IPAddress
ipaddress.ip_network = ipaddress.IPNetwork
@ -34,12 +34,15 @@ if sys.version_info >= (3, 4):
from importlib.util import cache_from_source
else:
import imp
uses_pycache = hasattr(imp, 'cache_from_source')
if uses_pycache:
cache_from_source = imp.cache_from_source
else:
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"

View File

@ -14,7 +14,7 @@ Some terminology:
import logging
import os
from pip._vendor.six import next
from pip._vendor import six
from pip._vendor.six.moves import configparser
from pip._internal.exceptions import ConfigurationError
@ -23,12 +23,20 @@ from pip._internal.locations import (
site_config_files, venv_config_file
)
from pip._internal.utils.misc import ensure_dir, enum
from pip._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('_', '-')
@ -38,6 +46,7 @@ def _normalize_name(name):
def _disassemble_key(name):
# type: (str) -> List[str]
return name.split(".", 1)
@ -66,6 +75,7 @@ class Configuration(object):
"""
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]
@ -75,8 +85,8 @@ class Configuration(object):
", ".join(map(repr, _valid_load_only[:-1]))
)
)
self.isolated = isolated
self.load_only = load_only
self.isolated = isolated # type: bool
self.load_only = load_only # type: Optional[Kind]
# The order here determines the override order.
self._override_order = [
@ -86,11 +96,16 @@ class Configuration(object):
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}
self._config = {variant: {} for variant in self._override_order}
self._modified_parsers = []
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()
@ -98,6 +113,7 @@ class Configuration(object):
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, \
@ -109,12 +125,14 @@ class Configuration(object):
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:
@ -123,6 +141,7 @@ class Configuration(object):
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()
@ -141,6 +160,7 @@ class Configuration(object):
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()
@ -161,7 +181,13 @@ class Configuration(object):
if modified_something:
# name removed from parser, section may now be empty
if next(iter(parser.items(section)), None) is None:
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)
@ -173,6 +199,7 @@ class Configuration(object):
del self._config[self.load_only][key]
def save(self):
# type: () -> None
"""Save the currentin-memory state.
"""
self._ensure_have_load_only()
@ -184,19 +211,21 @@ class Configuration(object):
ensure_dir(os.path.dirname(fname))
with open(fname, "w") as f:
parser.write(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
@ -209,6 +238,7 @@ class Configuration(object):
return retval
def _load_config_files(self):
# type: () -> None
"""Loads configuration from configuration files
"""
config_files = dict(self._iter_config_files())
@ -235,6 +265,7 @@ class Configuration(object):
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)
@ -245,6 +276,7 @@ class Configuration(object):
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.
@ -256,6 +288,7 @@ class Configuration(object):
return parser
def _load_environment_vars(self):
# type: () -> None
"""Loads configuration from environment variables
"""
self._config[kinds.ENV_VAR].update(
@ -263,6 +296,7 @@ class Configuration(object):
)
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
@ -275,6 +309,7 @@ class Configuration(object):
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 = (
@ -286,6 +321,7 @@ class Configuration(object):
# 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.
@ -315,6 +351,7 @@ class Configuration(object):
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:
@ -328,6 +365,7 @@ class Configuration(object):
# 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)

View File

@ -22,7 +22,9 @@ from pip._vendor.requests.models import CONTENT_CHUNK_SIZE, Response
from pip._vendor.requests.packages import urllib3
from pip._vendor.requests.structures import CaseInsensitiveDict
from pip._vendor.requests.utils import get_netrc_auth
from pip._vendor.six.moves import xmlrpc_client
# NOTE: XMLRPC Client is not annotated in typeshed as on 2017-07-17, which is
# why we ignore the type on this import
from pip._vendor.six.moves import xmlrpc_client # type: ignore
from pip._vendor.six.moves.urllib import parse as urllib_parse
from pip._vendor.six.moves.urllib import request as urllib_request
from pip._vendor.six.moves.urllib.parse import unquote as urllib_unquote

View File

@ -8,7 +8,7 @@ import site
import sys
import sysconfig
from distutils import sysconfig as distutils_sysconfig
from distutils.command.install import SCHEME_KEYS, install # noqa
from distutils.command.install import SCHEME_KEYS, install # type: ignore
from pip._internal.compat import WINDOWS, expanduser
from pip._internal.utils import appdirs

View File

@ -6,6 +6,11 @@ from __future__ import absolute_import
import logging
import warnings
from pip._internal.utils.typing import MYPY_CHECK_RUNNING
if MYPY_CHECK_RUNNING:
from typing import Any
class PipDeprecationWarning(Warning):
pass
@ -26,7 +31,7 @@ class RemovedInPip12Warning(PipDeprecationWarning, Pending):
# Warnings <-> Logging Integration
_warnings_showwarning = None
_warnings_showwarning = None # type: Any
def _showwarning(message, category, filename, lineno, file=None, line=None):

View File

@ -11,7 +11,7 @@ from pip._internal.utils.misc import ensure_dir
try:
import threading
except ImportError:
import dummy_threading as threading
import dummy_threading as threading # type: ignore
try:

View File

@ -19,7 +19,9 @@ import zipfile
from collections import deque
from pip._vendor import pkg_resources
from pip._vendor.retrying import retry
# NOTE: retrying is not annotated in typeshed as on 2017-07-17, which is
# why we ignore the type on this import.
from pip._vendor.retrying import retry # type: ignore
from pip._vendor.six import PY2
from pip._vendor.six.moves import input

View File

@ -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 pip.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

View File

@ -18,6 +18,10 @@ from pip._vendor.progress.spinner import Spinner
from pip._internal.compat import WINDOWS
from pip._internal.utils.logging import get_indentation
from pip._internal.utils.misc import format_size
from pip._internal.utils.typing import MYPY_CHECK_RUNNING
if MYPY_CHECK_RUNNING:
from typing import Any
try:
from pip._vendor import colorama
@ -56,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):
@ -125,7 +129,7 @@ class BlueEmojiBar(IncrementalBar):
suffix = "%(percent)d%%"
bar_prefix = " "
bar_suffix = " "
phases = (u"\U0001F539", u"\U0001F537", u"\U0001F535")
phases = (u"\U0001F539", u"\U0001F537", u"\U0001F535") # type: Any
class DownloadProgressMixin(object):
@ -194,36 +198,45 @@ class BaseDownloadProgressBar(WindowsMixin, InterruptibleMixin,
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):
class DefaultDownloadProgressBar(BaseDownloadProgressBar,
_BaseBar): # type: ignore
pass
class DownloadSilentBar(BaseDownloadProgressBar, SilentBar):
class DownloadSilentBar(BaseDownloadProgressBar, SilentBar): # type: ignore
pass
class DownloadIncrementalBar(BaseDownloadProgressBar, IncrementalBar):
class DownloadIncrementalBar(BaseDownloadProgressBar, # type: ignore
IncrementalBar):
pass
class DownloadChargingBar(BaseDownloadProgressBar, ChargingBar):
class DownloadChargingBar(BaseDownloadProgressBar, # type: ignore
ChargingBar):
pass
class DownloadShadyBar(BaseDownloadProgressBar, ShadyBar):
class DownloadShadyBar(BaseDownloadProgressBar, ShadyBar): # type: ignore
pass
class DownloadFillingSquaresBar(BaseDownloadProgressBar, FillingSquaresBar):
class DownloadFillingSquaresBar(BaseDownloadProgressBar, # type: ignore
FillingSquaresBar):
pass
class DownloadFillingCirclesBar(BaseDownloadProgressBar, FillingCirclesBar):
class DownloadFillingCirclesBar(BaseDownloadProgressBar, # type: ignore
FillingCirclesBar):
pass
class DownloadBlueEmojiProgressBar(BaseDownloadProgressBar, BlueEmojiBar):
class DownloadBlueEmojiProgressBar(BaseDownloadProgressBar, # type: ignore
BlueEmojiBar):
pass

View File

@ -13,7 +13,11 @@ from pip._internal.exceptions import BadCommand
from pip._internal.utils.misc import (
display_path, backup_dir, call_subprocess, rmtree, ask_path_exists,
)
from pip._internal.utils.typing import MYPY_CHECK_RUNNING
if MYPY_CHECK_RUNNING:
from typing import Dict, Tuple
from pip._internal.basecommand import Command
__all__ = ['vcs', 'get_src_requirement']
@ -22,7 +26,7 @@ logger = logging.getLogger(__name__)
class VcsSupport(object):
_registry = {}
_registry = {} # type: Dict[str, Command]
schemes = ['ssh', 'git', 'hg', 'bzr', 'sftp', 'svn']
def __init__(self):
@ -99,7 +103,7 @@ class VersionControl(object):
name = ''
dirname = ''
# List of supported schemes for this Version Control
schemes = ()
schemes = () # type: Tuple[str, ...]
def __init__(self, url=None, *args, **kwargs):
self.url = url

View File

@ -0,0 +1 @@
from appdirs import *

View File

@ -0,0 +1 @@
from cachecontrol import *

View File

@ -0,0 +1 @@
from colorama import *

View File

@ -0,0 +1 @@
from distlib import *

View File

@ -0,0 +1 @@
from distro import *

View File

@ -0,0 +1 @@
from html5lib import *

View File

@ -0,0 +1 @@
from ipaddress import *

View File

@ -0,0 +1 @@
from lockfile import *

View File

@ -0,0 +1 @@
from msgpack import *

View File

@ -0,0 +1 @@
from packaging import *

View File

@ -0,0 +1 @@
from pkg_resources import *

View File

@ -0,0 +1 @@
from progress import *

View File

@ -0,0 +1 @@
from pyparsing import *

View File

@ -0,0 +1 @@
from pytoml import *

View File

@ -0,0 +1 @@
from requests import *

View File

@ -0,0 +1 @@
from retrying import *

View File

@ -0,0 +1 @@
from six import *

View File

@ -0,0 +1 @@
from six.moves import *

View File

@ -0,0 +1 @@
from webencodings import *

View File

@ -1,6 +1,7 @@
""""Vendoring script, python 3.5 needed"""
from pathlib import Path
import os
import re
import shutil
@ -32,6 +33,11 @@ def log(msg):
print('[vendoring.%s] %s' % (TASK_NAME, msg))
def _get_vendor_dir(ctx):
git_root = ctx.run('git rev-parse --show-toplevel', hide=True).stdout
return Path(git_root.strip()) / 'src' / 'pip' / '_vendor'
def clean_vendor(ctx, vendor_dir):
# Old _vendor cleanup
remove_all(vendor_dir.glob('*.pyc'))
@ -45,6 +51,18 @@ def clean_vendor(ctx, vendor_dir):
log('Skipping %s' % item)
def detect_vendored_libs(vendor_dir):
retval = []
for item in vendor_dir.iterdir():
if item.is_dir():
retval.append(item.name)
elif item.name.endswith(".pyi"):
continue
elif item.name not in FILE_WHITE_LIST:
retval.append(item.name[:-3])
return retval
def rewrite_imports(package_dir, vendored_libs):
for item in package_dir.iterdir():
if item.is_dir():
@ -98,12 +116,7 @@ def vendor(ctx, vendor_dir):
remove_all(vendor_dir.glob('msgpack/*.so'))
# Detect the vendored packages/modules
vendored_libs = []
for item in vendor_dir.iterdir():
if item.is_dir():
vendored_libs.append(item.name)
elif item.name not in FILE_WHITE_LIST:
vendored_libs.append(item.name[:-3])
vendored_libs = detect_vendored_libs(vendor_dir)
log("Detected vendored libraries: %s" % ", ".join(vendored_libs))
# Global import rewrites
@ -121,12 +134,37 @@ def vendor(ctx, vendor_dir):
apply_patch(ctx, patch)
@invoke.task(name=TASK_NAME)
@invoke.task
def update_stubs(ctx):
vendor_dir = _get_vendor_dir(ctx)
vendored_libs = detect_vendored_libs(vendor_dir)
print("[vendoring.update_stubs] Add mypy stubs")
# Some projects need stubs other than a simple <name>.pyi
extra_stubs_needed = {
"six": ["six.__init__", "six.moves"]
}
for lib in vendored_libs:
if lib not in extra_stubs_needed:
(vendor_dir / (lib + ".pyi")).write_text("from %s import *" % lib)
continue
for selector in extra_stubs_needed[lib]:
fname = selector.replace(".", os.sep) + ".pyi"
if selector.endswith(".__init__"):
selector = selector[:-9]
f_path = vendor_dir / fname
if not f_path.parent.exists():
f_path.parent.mkdir()
f_path.write_text("from %s import *" % selector)
@invoke.task(name=TASK_NAME, post=[update_stubs])
def main(ctx):
git_root = Path(
ctx.run('git rev-parse --show-toplevel', hide=True).stdout.strip()
)
vendor_dir = git_root / 'src' / 'pip' / '_vendor'
vendor_dir = _get_vendor_dir(ctx)
log('Using vendor dir: %s' % vendor_dir)
clean_vendor(ctx, vendor_dir)
vendor(ctx, vendor_dir)

View File

@ -1,6 +1,6 @@
[tox]
envlist =
docs, packaging, lint-py2, lint-py3,
docs, packaging, lint-py2, lint-py3, mypy,
py27, py33, py34, py35, py36, py37, pypy
[testenv]
@ -44,3 +44,10 @@ commands = {[lint]commands}
basepython = python3
deps = {[lint]deps}
commands = {[lint]commands}
[testenv:mypy]
basepython = python3
deps = mypy
commands =
mypy src/pip
mypy src/pip -2